@wcstack/state 1.17.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/README.ja.md +57 -0
- package/README.md +57 -0
- package/dist/index.esm.js +1557 -243
- 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
|
|
|
@@ -150,6 +150,9 @@ const STATE_COMMAND_TOKENS_NAME = "$commandTokens";
|
|
|
150
150
|
const STATE_COMMAND_NAMESPACE_NAME = "$command";
|
|
151
151
|
const STATE_EVENT_TOKENS_NAME = "$eventTokens";
|
|
152
152
|
const STATE_ON_NAME = "$on";
|
|
153
|
+
const STATE_STREAMS_NAME = "$streams";
|
|
154
|
+
const STATE_STREAM_STATUS_NAMESPACE_NAME = "$streamStatus";
|
|
155
|
+
const STATE_STREAM_ERROR_NAMESPACE_NAME = "$streamError";
|
|
153
156
|
const DCC_DEFINITION_ATTRIBUTE = "data-wc-definition";
|
|
154
157
|
|
|
155
158
|
const _cache$4 = new Map();
|
|
@@ -1843,13 +1846,62 @@ const connectedCallbackSymbol = Symbol("$$connectedCallback");
|
|
|
1843
1846
|
const disconnectedCallbackSymbol = Symbol("$$disconnectedCallback");
|
|
1844
1847
|
const updatedCallbackSymbol = Symbol("$$updatedCallback");
|
|
1845
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
|
+
|
|
1846
1897
|
// onclick: $command.<name> のように、DOM イベントから command token を直接 emit する形式かを判定する。
|
|
1847
1898
|
// 右辺が $command 名前空間配下のパス($command.<token>)のときに true。
|
|
1848
1899
|
function isCommandTokenPath(statePathName) {
|
|
1849
1900
|
return statePathName.startsWith(STATE_COMMAND_NAMESPACE_NAME + ".");
|
|
1850
1901
|
}
|
|
1851
1902
|
const handlerByHandlerKey$3 = new Map();
|
|
1852
|
-
|
|
1903
|
+
// binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
|
|
1904
|
+
const bindingRegistry$3 = createHandlerBindingRegistry();
|
|
1853
1905
|
function getHandlerKey$3(binding) {
|
|
1854
1906
|
const modifierKey = binding.propModifiers.filter(m => m === 'prevent' || m === 'stop').sort().join(',');
|
|
1855
1907
|
return `${binding.stateName}::${binding.statePathName}::${modifierKey}`;
|
|
@@ -1898,14 +1950,7 @@ function attachEventHandler(binding) {
|
|
|
1898
1950
|
}
|
|
1899
1951
|
const eventName = binding.propName.slice(2);
|
|
1900
1952
|
binding.node.addEventListener(eventName, stateEventHandler);
|
|
1901
|
-
|
|
1902
|
-
if (typeof bindingSet === "undefined") {
|
|
1903
|
-
bindingSet = new Set([binding]);
|
|
1904
|
-
bindingSetByHandlerKey$3.set(key, bindingSet);
|
|
1905
|
-
}
|
|
1906
|
-
else {
|
|
1907
|
-
bindingSet.add(binding);
|
|
1908
|
-
}
|
|
1953
|
+
bindingRegistry$3.add(key, binding);
|
|
1909
1954
|
return true;
|
|
1910
1955
|
}
|
|
1911
1956
|
|
|
@@ -1914,12 +1959,12 @@ function attachEventHandler(binding) {
|
|
|
1914
1959
|
class EventToken extends Token {
|
|
1915
1960
|
}
|
|
1916
1961
|
|
|
1917
|
-
const registryByStateElement$
|
|
1962
|
+
const registryByStateElement$2 = new WeakMap();
|
|
1918
1963
|
function getOrCreateEventToken(stateElement, name) {
|
|
1919
|
-
let registry = registryByStateElement$
|
|
1964
|
+
let registry = registryByStateElement$2.get(stateElement);
|
|
1920
1965
|
if (typeof registry === "undefined") {
|
|
1921
1966
|
registry = new Map();
|
|
1922
|
-
registryByStateElement$
|
|
1967
|
+
registryByStateElement$2.set(stateElement, registry);
|
|
1923
1968
|
}
|
|
1924
1969
|
let token = registry.get(name);
|
|
1925
1970
|
if (typeof token === "undefined") {
|
|
@@ -1929,7 +1974,7 @@ function getOrCreateEventToken(stateElement, name) {
|
|
|
1929
1974
|
return token;
|
|
1930
1975
|
}
|
|
1931
1976
|
function clearEventTokenRegistry(stateElement) {
|
|
1932
|
-
registryByStateElement$
|
|
1977
|
+
registryByStateElement$2.delete(stateElement);
|
|
1933
1978
|
}
|
|
1934
1979
|
|
|
1935
1980
|
/**
|
|
@@ -2075,7 +2120,8 @@ function isPossibleTwoWay(node, propName) {
|
|
|
2075
2120
|
}
|
|
2076
2121
|
|
|
2077
2122
|
const handlerByHandlerKey$2 = new Map();
|
|
2078
|
-
|
|
2123
|
+
// binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
|
|
2124
|
+
const bindingRegistry$2 = createHandlerBindingRegistry();
|
|
2079
2125
|
const DEFAULT_GETTER = (e) => e.detail;
|
|
2080
2126
|
function getHandlerKey$2(binding, eventName, hasGetter) {
|
|
2081
2127
|
const filterKey = binding.inFilters.map(f => f.filterName + '(' + f.args.join(',') + ')').join('|');
|
|
@@ -2178,14 +2224,7 @@ function attachTwowayEventHandler(binding) {
|
|
|
2178
2224
|
handlerByHandlerKey$2.set(key, twowayEventHandler);
|
|
2179
2225
|
}
|
|
2180
2226
|
binding.node.addEventListener(eventName, twowayEventHandler);
|
|
2181
|
-
|
|
2182
|
-
if (typeof bindingSet === "undefined") {
|
|
2183
|
-
bindingSet = new Set([binding]);
|
|
2184
|
-
bindingSetByHandlerKey$2.set(key, bindingSet);
|
|
2185
|
-
}
|
|
2186
|
-
else {
|
|
2187
|
-
bindingSet.add(binding);
|
|
2188
|
-
}
|
|
2227
|
+
bindingRegistry$2.add(key, binding);
|
|
2189
2228
|
}
|
|
2190
2229
|
}
|
|
2191
2230
|
|
|
@@ -2890,6 +2929,103 @@ function calcDiffIndexes(oldList, newList, oldIndexes, newIndexes, indexByValue)
|
|
|
2890
2929
|
};
|
|
2891
2930
|
}
|
|
2892
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
|
+
|
|
2893
3029
|
const bindingSetByAbsoluteStateAddress = new WeakMap();
|
|
2894
3030
|
function getBindingSetByAbsoluteStateAddress(absoluteStateAddress) {
|
|
2895
3031
|
let bindingSet = null;
|
|
@@ -2900,38 +3036,24 @@ function getBindingSetByAbsoluteStateAddress(absoluteStateAddress) {
|
|
|
2900
3036
|
}
|
|
2901
3037
|
return bindingSet;
|
|
2902
3038
|
}
|
|
3039
|
+
/**
|
|
3040
|
+
* 参照専用の取得。get-or-create と違い、未登録アドレスに空 Set を
|
|
3041
|
+
* 生成・キャッシュしない(リスト置換の drain は大量のバインディング無し
|
|
3042
|
+
* アドレスを照会するため、生成すると空 Set が溜まり続ける)。
|
|
3043
|
+
*/
|
|
3044
|
+
function peekBindingSetByAbsoluteStateAddress(absoluteStateAddress) {
|
|
3045
|
+
return bindingSetByAbsoluteStateAddress.get(absoluteStateAddress);
|
|
3046
|
+
}
|
|
2903
3047
|
function addBindingByAbsoluteStateAddress(absoluteStateAddress, binding) {
|
|
2904
3048
|
const bindingSet = getBindingSetByAbsoluteStateAddress(absoluteStateAddress);
|
|
2905
3049
|
bindingSet.add(binding);
|
|
2906
3050
|
}
|
|
2907
3051
|
function removeBindingByAbsoluteStateAddress(absoluteStateAddress, binding) {
|
|
2908
|
-
|
|
2909
|
-
bindingSet.
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
const stateAddressByBindingInfo = new WeakMap();
|
|
2913
|
-
function getStateAddressByBindingInfo(bindingInfo) {
|
|
2914
|
-
let stateAddress = null;
|
|
2915
|
-
stateAddress = stateAddressByBindingInfo.get(bindingInfo) || null;
|
|
2916
|
-
if (stateAddress !== null) {
|
|
2917
|
-
return stateAddress;
|
|
2918
|
-
}
|
|
2919
|
-
if (bindingInfo.statePathInfo.wildcardCount > 0) {
|
|
2920
|
-
const listIndex = getListIndexByBindingInfo(bindingInfo);
|
|
2921
|
-
if (listIndex === null) {
|
|
2922
|
-
raiseError(`Cannot resolve state address for binding with wildcard statePathName "${bindingInfo.statePathName}" because list index is null.`);
|
|
2923
|
-
}
|
|
2924
|
-
stateAddress = createStateAddress(bindingInfo.statePathInfo, listIndex);
|
|
2925
|
-
}
|
|
2926
|
-
else {
|
|
2927
|
-
stateAddress = createStateAddress(bindingInfo.statePathInfo, null);
|
|
3052
|
+
// get-or-create を通すと未登録アドレスに空 Set を生成してしまうため素の get で参照する
|
|
3053
|
+
const bindingSet = bindingSetByAbsoluteStateAddress.get(absoluteStateAddress);
|
|
3054
|
+
if (bindingSet !== undefined) {
|
|
3055
|
+
bindingSet.delete(binding);
|
|
2928
3056
|
}
|
|
2929
|
-
stateAddressByBindingInfo.set(bindingInfo, stateAddress);
|
|
2930
|
-
return stateAddress;
|
|
2931
|
-
}
|
|
2932
|
-
// call for change loopContext
|
|
2933
|
-
function clearStateAddressByBindingInfo(bindingInfo) {
|
|
2934
|
-
stateAddressByBindingInfo.delete(bindingInfo);
|
|
2935
3057
|
}
|
|
2936
3058
|
|
|
2937
3059
|
const bindingsByContent = new WeakMap();
|
|
@@ -2980,8 +3102,10 @@ function deactivateContent(content) {
|
|
|
2980
3102
|
for (const binding of bindings) {
|
|
2981
3103
|
const absoluteStateAddress = getAbsoluteStateAddressByBinding(binding);
|
|
2982
3104
|
removeBindingByAbsoluteStateAddress(absoluteStateAddress, binding);
|
|
2983
|
-
|
|
2984
|
-
|
|
3105
|
+
// アドレスキャッシュ(absoluteStateAddressByBinding / stateAddressByBindingInfo)
|
|
3106
|
+
// のクリアはここでは行わない。deactivateContent の呼び出し元(for/if)は必ず
|
|
3107
|
+
// 直後に content.unmount() を呼び、unmount が同じ2台帳をネスト content も含めて
|
|
3108
|
+
// クリアする(createContent.ts)。ここで消すと全 binding で二重 delete になる。
|
|
2985
3109
|
}
|
|
2986
3110
|
unbindLoopContextToContent(content);
|
|
2987
3111
|
}
|
|
@@ -3008,6 +3132,40 @@ function getContentSetByNode(node) {
|
|
|
3008
3132
|
}
|
|
3009
3133
|
return EMPTY_SET;
|
|
3010
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
|
+
}
|
|
3011
3169
|
|
|
3012
3170
|
const recursiveBindingTypes = new Set(['if', 'elseif', 'else', 'for']);
|
|
3013
3171
|
class Content {
|
|
@@ -3120,14 +3278,27 @@ function hydrateSetLastNode(node, lastNode) {
|
|
|
3120
3278
|
function getPooledContents(bindingInfo) {
|
|
3121
3279
|
return pooledContentsByNode.get(bindingInfo.node) || [];
|
|
3122
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;
|
|
3123
3288
|
function setPooledContent(bindingInfo, content) {
|
|
3124
|
-
|
|
3289
|
+
let contents = pooledContentsByNode.get(bindingInfo.node);
|
|
3125
3290
|
if (typeof contents === 'undefined') {
|
|
3126
|
-
|
|
3291
|
+
contents = [];
|
|
3292
|
+
pooledContentsByNode.set(bindingInfo.node, contents);
|
|
3127
3293
|
}
|
|
3128
|
-
|
|
3294
|
+
if (contents.length < maxPooledContents) {
|
|
3129
3295
|
contents.push(content);
|
|
3130
3296
|
}
|
|
3297
|
+
else {
|
|
3298
|
+
// 上限超過: content を完全に手放す。contentSetByNode は createContent 時に
|
|
3299
|
+
// 追加されたきり解放経路が無いため、ここで外さないと GC できない。
|
|
3300
|
+
deleteContentByNode(bindingInfo.node, content);
|
|
3301
|
+
}
|
|
3131
3302
|
}
|
|
3132
3303
|
function isOnlyNodeInParentContent(firstNode, lastNode) {
|
|
3133
3304
|
let prevCheckNode = firstNode.previousSibling;
|
|
@@ -3151,6 +3322,23 @@ function isOnlyNodeInParentContent(firstNode, lastNode) {
|
|
|
3151
3322
|
}
|
|
3152
3323
|
return onlyNode;
|
|
3153
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
|
+
}
|
|
3154
3342
|
function getContent(node, listIndex) {
|
|
3155
3343
|
let contentByListIndex = contentByListIndexByNode.get(node);
|
|
3156
3344
|
if (typeof contentByListIndex === 'undefined') {
|
|
@@ -3210,6 +3398,11 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
3210
3398
|
let lastNode = bindingInfo.node;
|
|
3211
3399
|
const elementPathInfo = getPathInfo(listPathInfo.path + '.' + WILDCARD);
|
|
3212
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);
|
|
3213
3406
|
let fragment = null;
|
|
3214
3407
|
if (diff.newIndexes.length == diff.addIndexSet.size
|
|
3215
3408
|
&& diff.newIndexes.length > 0
|
|
@@ -3279,7 +3472,13 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
3279
3472
|
if (content === null) {
|
|
3280
3473
|
raiseError(`Content not found for ListIndex: ${index.index} at path "${listPathInfo.path}"`);
|
|
3281
3474
|
}
|
|
3282
|
-
|
|
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) {
|
|
3283
3482
|
content.mountAfter(lastNode);
|
|
3284
3483
|
}
|
|
3285
3484
|
}
|
|
@@ -3608,8 +3807,14 @@ function applyChangeToStyle(binding, _context, newValue) {
|
|
|
3608
3807
|
|
|
3609
3808
|
const ssrWrappedNodes = new WeakSet();
|
|
3610
3809
|
function applyChangeToText(binding, _context, newValue) {
|
|
3611
|
-
|
|
3612
|
-
|
|
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;
|
|
3613
3818
|
}
|
|
3614
3819
|
// SSR モード時: テキストノードの前後にコメントを挿入して境界を明示
|
|
3615
3820
|
if (inSsr() && !ssrWrappedNodes.has(binding.replaceNode)) {
|
|
@@ -3669,6 +3874,37 @@ function getValue(state, binding) {
|
|
|
3669
3874
|
}
|
|
3670
3875
|
}
|
|
3671
3876
|
|
|
3877
|
+
// applyChange が「未 define のカスタム要素」への適用を見送った binding の台帳。
|
|
3878
|
+
// define されるまでの間、同じ binding に対して applyChange は(state 更新の
|
|
3879
|
+
// たびに)何度も呼ばれうるため、whenDefined の多重登録をここで抑止する。
|
|
3880
|
+
// WeakSet なので binding の寿命に追従し、恒久 define されないタグでもリークしない。
|
|
3881
|
+
const scheduledBindings = new WeakSet();
|
|
3882
|
+
/**
|
|
3883
|
+
* 未 define のカスタム要素に対する適用を customElements.whenDefined 後に再実行
|
|
3884
|
+
* する。two-way / event-token の attach、spread の deferred 展開はいずれも
|
|
3885
|
+
* whenDefined で再試行するのに対し、値の適用だけが片道 skip だった非対称の解消
|
|
3886
|
+
* (docs/state-binding-init-races.md §2)。
|
|
3887
|
+
*
|
|
3888
|
+
* 再適用は applyChangeFromBindings を通すため、define 時点の最新 state 値で
|
|
3889
|
+
* 適用される(skip 時点の値を保持しない)。define を待つ間に DOM から外れた
|
|
3890
|
+
* binding には適用しない(deferred spread と同じ規約)。
|
|
3891
|
+
*/
|
|
3892
|
+
function scheduleDeferredApply(binding, tagName) {
|
|
3893
|
+
if (scheduledBindings.has(binding)) {
|
|
3894
|
+
return;
|
|
3895
|
+
}
|
|
3896
|
+
scheduledBindings.add(binding);
|
|
3897
|
+
customElements.whenDefined(tagName).then(() => {
|
|
3898
|
+
scheduledBindings.delete(binding);
|
|
3899
|
+
if (!binding.replaceNode.isConnected) {
|
|
3900
|
+
return; // define を待つ間にノードが削除された
|
|
3901
|
+
}
|
|
3902
|
+
applyChangeFromBindings([binding]);
|
|
3903
|
+
}).catch((error) => {
|
|
3904
|
+
console.error(`[@wcstack/state] deferred apply failed for <${tagName}>.`, error);
|
|
3905
|
+
});
|
|
3906
|
+
}
|
|
3907
|
+
|
|
3672
3908
|
const applyChangeByFirstSegment = {
|
|
3673
3909
|
"class": applyChangeToClass,
|
|
3674
3910
|
"attr": applyChangeToAttribute,
|
|
@@ -3749,15 +3985,20 @@ function applyChange(binding, context) {
|
|
|
3749
3985
|
return;
|
|
3750
3986
|
}
|
|
3751
3987
|
context.appliedBindingSet.add(binding);
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
absAddress
|
|
3760
|
-
|
|
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
|
+
}
|
|
3761
4002
|
}
|
|
3762
4003
|
if (binding.bindingType === "event") {
|
|
3763
4004
|
return;
|
|
@@ -3765,10 +4006,22 @@ function applyChange(binding, context) {
|
|
|
3765
4006
|
const customTag = getCustomElement(binding.replaceNode);
|
|
3766
4007
|
if (customTag) {
|
|
3767
4008
|
if (customElements.get(customTag) === undefined) {
|
|
3768
|
-
//
|
|
4009
|
+
// 未 define のカスタム要素へは今は適用できない(accessor 未確立の要素に
|
|
4010
|
+
// 素の own property を書くと upgrade 後に class accessor を隠してしまう)。
|
|
4011
|
+
// whenDefined 後に最新 state 値で再適用する(two-way attach / deferred
|
|
4012
|
+
// spread と対称。docs/state-binding-init-races.md §2)。
|
|
4013
|
+
scheduleDeferredApply(binding, customTag);
|
|
3769
4014
|
return;
|
|
3770
4015
|
}
|
|
3771
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
|
+
}
|
|
3772
4025
|
let rootNode = binding.replaceNode.getRootNode();
|
|
3773
4026
|
if (rootNode instanceof DocumentFragment && !(rootNode instanceof ShadowRoot)) {
|
|
3774
4027
|
rootNode = getRootNodeByFragment(rootNode);
|
|
@@ -3849,6 +4102,9 @@ function applyChangeFromBindings(bindings) {
|
|
|
3849
4102
|
newListValueByAbsAddress: newListValueByAbsAddress,
|
|
3850
4103
|
updatedAbsAddressSetByStateElement: updatedAbsAddressSetByStateElement,
|
|
3851
4104
|
deferredSelectBindings: deferredSelectBindings,
|
|
4105
|
+
// グループ内の binding は下の do/while が「解決済みルート === rootNode」を
|
|
4106
|
+
// 検証してから applyChange に渡す(applyChange 側の getRootNode 省略の根拠)
|
|
4107
|
+
sameRootVerified: true,
|
|
3852
4108
|
};
|
|
3853
4109
|
do {
|
|
3854
4110
|
applyChange(binding, context);
|
|
@@ -3879,7 +4135,8 @@ function applyChangeFromBindings(bindings) {
|
|
|
3879
4135
|
}
|
|
3880
4136
|
|
|
3881
4137
|
const handlerByHandlerKey$1 = new Map();
|
|
3882
|
-
|
|
4138
|
+
// binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
|
|
4139
|
+
const bindingRegistry$1 = createHandlerBindingRegistry();
|
|
3883
4140
|
function getHandlerKey$1(binding, eventName) {
|
|
3884
4141
|
const filterKey = binding.inFilters.map(f => f.filterName + '(' + f.args.join(',') + ')').join('|');
|
|
3885
4142
|
return `${binding.stateName}::${binding.statePathName}::${eventName}::${filterKey}`;
|
|
@@ -3933,21 +4190,15 @@ function attachRadioEventHandler(binding) {
|
|
|
3933
4190
|
handlerByHandlerKey$1.set(key, radioEventHandler);
|
|
3934
4191
|
}
|
|
3935
4192
|
binding.node.addEventListener(eventName, radioEventHandler);
|
|
3936
|
-
|
|
3937
|
-
if (typeof bindingSet === "undefined") {
|
|
3938
|
-
bindingSet = new Set([binding]);
|
|
3939
|
-
bindingSetByHandlerKey$1.set(key, bindingSet);
|
|
3940
|
-
}
|
|
3941
|
-
else {
|
|
3942
|
-
bindingSet.add(binding);
|
|
3943
|
-
}
|
|
4193
|
+
bindingRegistry$1.add(key, binding);
|
|
3944
4194
|
return true;
|
|
3945
4195
|
}
|
|
3946
4196
|
return false;
|
|
3947
4197
|
}
|
|
3948
4198
|
|
|
3949
4199
|
const handlerByHandlerKey = new Map();
|
|
3950
|
-
|
|
4200
|
+
// binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
|
|
4201
|
+
const bindingRegistry = createHandlerBindingRegistry();
|
|
3951
4202
|
function getHandlerKey(binding, eventName) {
|
|
3952
4203
|
const filterKey = binding.inFilters.map(f => f.filterName + '(' + f.args.join(',') + ')').join('|');
|
|
3953
4204
|
return `${binding.stateName}::${binding.statePathName}::${eventName}::${filterKey}`;
|
|
@@ -4020,14 +4271,7 @@ function attachCheckboxEventHandler(binding) {
|
|
|
4020
4271
|
handlerByHandlerKey.set(key, checkboxEventHandler);
|
|
4021
4272
|
}
|
|
4022
4273
|
binding.node.addEventListener(eventName, checkboxEventHandler);
|
|
4023
|
-
|
|
4024
|
-
if (typeof bindingSet === "undefined") {
|
|
4025
|
-
bindingSet = new Set([binding]);
|
|
4026
|
-
bindingSetByHandlerKey.set(key, bindingSet);
|
|
4027
|
-
}
|
|
4028
|
-
else {
|
|
4029
|
-
bindingSet.add(binding);
|
|
4030
|
-
}
|
|
4274
|
+
bindingRegistry.add(key, binding);
|
|
4031
4275
|
return true;
|
|
4032
4276
|
}
|
|
4033
4277
|
return false;
|
|
@@ -5486,12 +5730,12 @@ function processCommandTokensDeclaration(state) {
|
|
|
5486
5730
|
return names;
|
|
5487
5731
|
}
|
|
5488
5732
|
|
|
5489
|
-
const registryByStateElement = new WeakMap();
|
|
5733
|
+
const registryByStateElement$1 = new WeakMap();
|
|
5490
5734
|
function getOrCreateCommandToken(stateElement, name) {
|
|
5491
|
-
let registry = registryByStateElement.get(stateElement);
|
|
5735
|
+
let registry = registryByStateElement$1.get(stateElement);
|
|
5492
5736
|
if (typeof registry === "undefined") {
|
|
5493
5737
|
registry = new Map();
|
|
5494
|
-
registryByStateElement.set(stateElement, registry);
|
|
5738
|
+
registryByStateElement$1.set(stateElement, registry);
|
|
5495
5739
|
}
|
|
5496
5740
|
let token = registry.get(name);
|
|
5497
5741
|
if (typeof token === "undefined") {
|
|
@@ -5501,7 +5745,7 @@ function getOrCreateCommandToken(stateElement, name) {
|
|
|
5501
5745
|
return token;
|
|
5502
5746
|
}
|
|
5503
5747
|
function clearCommandTokenRegistry(stateElement) {
|
|
5504
|
-
registryByStateElement.delete(stateElement);
|
|
5748
|
+
registryByStateElement$1.delete(stateElement);
|
|
5505
5749
|
}
|
|
5506
5750
|
|
|
5507
5751
|
/**
|
|
@@ -5623,95 +5867,1005 @@ function processOnDeclaration(stateElement, state, eventTokenNames) {
|
|
|
5623
5867
|
}
|
|
5624
5868
|
}
|
|
5625
5869
|
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5870
|
+
/**
|
|
5871
|
+
* stream/lastNotified.ts
|
|
5872
|
+
*
|
|
5873
|
+
* 「最後に通知した観測値」台帳 — DOM binding / $updatedCallback(観測層)が
|
|
5874
|
+
* 最後に見た status・error(docs/state-streams-design.md §4-3)。
|
|
5875
|
+
*
|
|
5876
|
+
* 通知の same-value 判定を entry フィールドとの比較で行うと、再 set
|
|
5877
|
+
* (clearStreamRegistry → 新 entry 生成)を跨いだ陳腐化を検出できない
|
|
5878
|
+
* (error 表示中に再 set すると新 entry は error=null で生まれるため
|
|
5879
|
+
* null → null と誤判定して $postUpdate が落ち、DOM に旧 error が残る)。
|
|
5880
|
+
* そのため通知 dedup は entry の寿命ではなく stateElement の寿命で持つ
|
|
5881
|
+
* (ただし再 set で新宣言から消えた名前のエントリは pruneLastNotified で削除する —
|
|
5882
|
+
* 同名にしか dedup は要らず、放置すると台帳が単調増加するため)。
|
|
5883
|
+
* 未通知(初回)の基準値は宣言直後の観測初期値と同じ { idle, null }。
|
|
5884
|
+
*
|
|
5885
|
+
* さらに abortAllStreams(§5-1)は registry entry を通知なしで idle / null に
|
|
5886
|
+
* 直接ミューテーションするため、観測層が「台帳の値」と「idle / null」の
|
|
5887
|
+
* どちらを見たか確定できなくなる(binding / computed の fresh 読みは通知が
|
|
5888
|
+
* なくても他パスの drain で走る)。その乖離フィールドは invalidateLastNotified
|
|
5889
|
+
* で UNCERTAIN に無効化し、次回 updateStreamStatus の同値判定が必ず
|
|
5890
|
+
* 「変化あり」になるようにする(再接続ウィンドウ内の idle 描画が恒久陳腐化
|
|
5891
|
+
* しないための不変条件、§4-3)。
|
|
5892
|
+
*/
|
|
5893
|
+
/**
|
|
5894
|
+
* 無通知ミューテーション後の「観測値が確定できない」印。
|
|
5895
|
+
* どの実値とも一致しないため、次回の通知 dedup(`!==` / `Object.is`)を強制的に解除する。
|
|
5896
|
+
*/
|
|
5897
|
+
const UNCERTAIN = Symbol("wcs-stream-last-notified-uncertain");
|
|
5898
|
+
const lastNotifiedByStateElement = new WeakMap();
|
|
5899
|
+
/**
|
|
5900
|
+
* 最後に通知した観測値を返す。未通知なら基準値 { idle, null }。
|
|
5901
|
+
*/
|
|
5902
|
+
function getLastNotified(stateElement, name) {
|
|
5903
|
+
return (lastNotifiedByStateElement.get(stateElement)?.get(name) ?? { status: "idle", error: null });
|
|
5655
5904
|
}
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
return stateEl.createStateAsync("writable", async (state) => {
|
|
5665
|
-
result = await state[name](...args);
|
|
5666
|
-
}).then(() => result);
|
|
5667
|
-
});
|
|
5668
|
-
};
|
|
5905
|
+
/**
|
|
5906
|
+
* 通知した観測値を記録する(updateStreamStatus が $postUpdate 発行と同時に呼ぶ)。
|
|
5907
|
+
*/
|
|
5908
|
+
function setLastNotified(stateElement, name, status, error) {
|
|
5909
|
+
let lastMap = lastNotifiedByStateElement.get(stateElement);
|
|
5910
|
+
if (typeof lastMap === "undefined") {
|
|
5911
|
+
lastMap = new Map();
|
|
5912
|
+
lastNotifiedByStateElement.set(stateElement, lastMap);
|
|
5669
5913
|
}
|
|
5670
|
-
|
|
5671
|
-
const stateEl = this.stateElement;
|
|
5672
|
-
if (!stateEl)
|
|
5673
|
-
return undefined;
|
|
5674
|
-
return stateEl.initializePromise.then(() => {
|
|
5675
|
-
let result;
|
|
5676
|
-
stateEl.createState("writable", (state) => {
|
|
5677
|
-
result = state[name](...args);
|
|
5678
|
-
});
|
|
5679
|
-
return result;
|
|
5680
|
-
});
|
|
5681
|
-
};
|
|
5682
|
-
}
|
|
5683
|
-
function isInternalProperty(name) {
|
|
5684
|
-
return name.startsWith("$");
|
|
5685
|
-
}
|
|
5686
|
-
|
|
5687
|
-
function createWcBindable(tagName, bindables) {
|
|
5688
|
-
const properties = bindables.map((propName) => ({
|
|
5689
|
-
name: propName,
|
|
5690
|
-
event: `${tagName}:${propName}-changed`,
|
|
5691
|
-
}));
|
|
5692
|
-
return {
|
|
5693
|
-
protocol: "wc-bindable",
|
|
5694
|
-
version: 1,
|
|
5695
|
-
properties,
|
|
5696
|
-
};
|
|
5914
|
+
lastMap.set(name, { status, error });
|
|
5697
5915
|
}
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5916
|
+
/**
|
|
5917
|
+
* 再 set(clearStreamRegistry → processStreamsDeclaration)後に呼び、新宣言に
|
|
5918
|
+
* 存在しない名前の台帳エントリを削除する。台帳は stateElement の寿命で生存するが
|
|
5919
|
+
* (§4-3 の再 set・再接続跨ぎ dedup)、それが必要なのは同名エントリのみで、
|
|
5920
|
+
* 旧宣言にしか無い名前は以後どの通知経路(updateStreamStatus)からも参照されない。
|
|
5921
|
+
* prune しないと、再 set のたびに異なる stream 名を使うステートで台帳が
|
|
5922
|
+
* stateElement の寿命の間単調増加する。
|
|
5923
|
+
* 既知の許容: prune 後に同名を再宣言した場合、dedup は基準値 { idle, null } から
|
|
5924
|
+
* やり直しになる(宣言削除時の binding 陳腐化が §4-4 の既知エッジである以上、
|
|
5925
|
+
* 再宣言は新規宣言と同じ扱いでよい)。
|
|
5926
|
+
*/
|
|
5927
|
+
function pruneLastNotified(stateElement, liveNames) {
|
|
5928
|
+
const lastMap = lastNotifiedByStateElement.get(stateElement);
|
|
5929
|
+
if (typeof lastMap === "undefined") {
|
|
5930
|
+
return;
|
|
5931
|
+
}
|
|
5932
|
+
for (const name of lastMap.keys()) {
|
|
5933
|
+
if (!liveNames.has(name)) {
|
|
5934
|
+
lastMap.delete(name);
|
|
5935
|
+
}
|
|
5702
5936
|
}
|
|
5703
|
-
return map;
|
|
5704
5937
|
}
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5938
|
+
/**
|
|
5939
|
+
* 無通知ミューテーション(abortAllStreams の idle / null 直接書き換え)の直後に呼び、
|
|
5940
|
+
* 台帳のうちミューテーション後の値と一致しないフィールドを UNCERTAIN に無効化する。
|
|
5941
|
+
* 一致しているフィールド(観測層がどちらを見ても同じ値)は dedup を維持する
|
|
5942
|
+
* (例: error が null のままなら再接続時に $streamError.<name> の余計な通知は出ない)。
|
|
5943
|
+
*/
|
|
5944
|
+
function invalidateLastNotified(stateElement, name) {
|
|
5945
|
+
const lastMap = lastNotifiedByStateElement.get(stateElement);
|
|
5946
|
+
if (typeof lastMap === "undefined") {
|
|
5947
|
+
return;
|
|
5711
5948
|
}
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5949
|
+
const last = lastMap.get(name);
|
|
5950
|
+
if (typeof last === "undefined") {
|
|
5951
|
+
// 未通知: 基準値 { idle, null } はミューテーション後の値と一致するため乖離しない
|
|
5952
|
+
return;
|
|
5953
|
+
}
|
|
5954
|
+
lastMap.set(name, {
|
|
5955
|
+
status: last.status === "idle" ? last.status : UNCERTAIN,
|
|
5956
|
+
error: Object.is(last.error, null) ? null : UNCERTAIN,
|
|
5957
|
+
});
|
|
5958
|
+
}
|
|
5959
|
+
|
|
5960
|
+
/**
|
|
5961
|
+
* stream/activeStateElements.ts
|
|
5962
|
+
*
|
|
5963
|
+
* 起動中(startStreams 済み・未切断)の stateElement の列挙用 Set
|
|
5964
|
+
* (docs/state-streams-design.md §3-2)。
|
|
5965
|
+
*
|
|
5966
|
+
* streamRegistry の WeakMap は列挙不能のため、updater の drain リスナーが
|
|
5967
|
+
* 「どの stateElement の entry と batch を交差させるか」を知るには
|
|
5968
|
+
* 列挙可能な strong Set が別途必要になる。lastNotified.ts と同じ
|
|
5969
|
+
* 「import 循環回避の小モジュール」パターン
|
|
5970
|
+
* (streamRegistry → activeStateElements ← streamRuntime の一方向依存に保つ)。
|
|
5971
|
+
*
|
|
5972
|
+
* リーク防止の不変条件(strong Set が切断済み要素の GC を妨げないための連動):
|
|
5973
|
+
* - add は startStreams(streamRuntime.ts)だけが行う
|
|
5974
|
+
* (eager 起動=connect 時、および接続中の `_state` 再 set 時の再起動)。
|
|
5975
|
+
* - delete は abortAllStreams / clearStreamRegistry(streamRegistry.ts)が行う。
|
|
5976
|
+
* disconnect(disconnectedCallback → abortAllStreams)と `_state` 再 set
|
|
5977
|
+
* (clearStreamRegistry → processStreamsDeclaration → 接続中なら startStreams で
|
|
5978
|
+
* 再 add)の両経路が必ずここを通るため、「Set に居る = 接続中かつ起動済み」が
|
|
5979
|
+
* 常に保たれ、切断済み stateElement への強参照は残らない。
|
|
5980
|
+
* 設計書 §3-2 の「未接続(disconnect 済み)の stateElement の entry は restart
|
|
5981
|
+
* しない」はこの不変条件で担保される。
|
|
5982
|
+
*/
|
|
5983
|
+
const activeStateElements = new Set();
|
|
5984
|
+
/**
|
|
5985
|
+
* 起動中 stateElement として登録する(startStreams 専用。不変条件はモジュールヘッダ参照)。
|
|
5986
|
+
*/
|
|
5987
|
+
function addActiveStateElement(stateElement) {
|
|
5988
|
+
activeStateElements.add(stateElement);
|
|
5989
|
+
}
|
|
5990
|
+
/**
|
|
5991
|
+
* 起動中 stateElement から外す(abortAllStreams / clearStreamRegistry 専用)。
|
|
5992
|
+
*/
|
|
5993
|
+
function deleteActiveStateElement(stateElement) {
|
|
5994
|
+
activeStateElements.delete(stateElement);
|
|
5995
|
+
}
|
|
5996
|
+
/**
|
|
5997
|
+
* 起動中 stateElement を列挙する(drain リスナーの交差判定用)。
|
|
5998
|
+
*/
|
|
5999
|
+
function getActiveStateElements() {
|
|
6000
|
+
return activeStateElements;
|
|
6001
|
+
}
|
|
6002
|
+
|
|
6003
|
+
/**
|
|
6004
|
+
* stream/streamRegistry.ts
|
|
6005
|
+
*
|
|
6006
|
+
* `$streams` の registry(docs/state-streams-design.md §2-1 / §5)。
|
|
6007
|
+
* eventTokenRegistry と対称の WeakMap registry。
|
|
6008
|
+
*
|
|
6009
|
+
* - status / error の正本は registry entry(state オブジェクト上に実プロパティは持たない)。
|
|
6010
|
+
* - disconnect 時は abortAllStreams(abort のみ・registry 保持)、
|
|
6011
|
+
* `_state` 再 set 時のみ clearStreamRegistry(abort + 全削除)。
|
|
6012
|
+
*/
|
|
6013
|
+
const registryByStateElement = new WeakMap();
|
|
6014
|
+
/**
|
|
6015
|
+
* stream entry 群を置換登録する(`_state` セッターからの再構築で丸ごと差し替える)。
|
|
6016
|
+
*/
|
|
6017
|
+
function setStreamEntries(stateElement, entries) {
|
|
6018
|
+
registryByStateElement.set(stateElement, entries);
|
|
6019
|
+
}
|
|
6020
|
+
/**
|
|
6021
|
+
* 登録済みの stream entry 群を返す。未登録なら空 Map を返す(registry への登録はしない)。
|
|
6022
|
+
*/
|
|
6023
|
+
function getStreamEntries(stateElement) {
|
|
6024
|
+
return registryByStateElement.get(stateElement) ?? new Map();
|
|
6025
|
+
}
|
|
6026
|
+
/**
|
|
6027
|
+
* 全 stream を abort して idle に戻す(設計書 §5-1)。registry は保持する。
|
|
6028
|
+
*
|
|
6029
|
+
* disconnectedCallback(切断時)に呼ばれるため、status / error の反映は
|
|
6030
|
+
* proxy / $postUpdate を使わず entry への直接ミューテーションで行う
|
|
6031
|
+
* (切断済みで binding 更新は不要かつ rootNode が無い)。
|
|
6032
|
+
*
|
|
6033
|
+
* 無通知ミューテーションは「最後に通知した観測値」台帳(stream/lastNotified.ts)
|
|
6034
|
+
* と registry を乖離させるため、同時に台帳側を invalidate する。これを怠ると
|
|
6035
|
+
* 再接続ウィンドウ内の fresh 読み(他パスの drain での getter 再計算など)が
|
|
6036
|
+
* 描画した idle に対し、restart の updateStreamStatus("active") が切断前の
|
|
6037
|
+
* 通知値と同値判定されて skip され、DOM が恒久的に陳腐化する(設計書 §4-3)。
|
|
6038
|
+
*/
|
|
6039
|
+
function abortAllStreams(stateElement) {
|
|
6040
|
+
// 依存駆動 restart の対象から外す(切断済み stateElement は restart しない、
|
|
6041
|
+
// 設計書 §3-2。add 側は startStreams — stream/activeStateElements.ts の
|
|
6042
|
+
// リーク防止不変条件を参照)。registry の有無に関わらず必ず外す。
|
|
6043
|
+
deleteActiveStateElement(stateElement);
|
|
6044
|
+
const entries = registryByStateElement.get(stateElement);
|
|
6045
|
+
if (typeof entries === "undefined") {
|
|
6046
|
+
return;
|
|
6047
|
+
}
|
|
6048
|
+
for (const entry of entries.values()) {
|
|
6049
|
+
entry.controller?.abort();
|
|
6050
|
+
entry.controller = null;
|
|
6051
|
+
entry.status = "idle";
|
|
6052
|
+
entry.error = null;
|
|
6053
|
+
invalidateLastNotified(stateElement, entry.name);
|
|
6054
|
+
}
|
|
6055
|
+
}
|
|
6056
|
+
/**
|
|
6057
|
+
* 全 stream を abort したうえで registry から削除する(`_state` 再 set 時の再配線用、設計書 §5-2)。
|
|
6058
|
+
*/
|
|
6059
|
+
function clearStreamRegistry(stateElement) {
|
|
6060
|
+
abortAllStreams(stateElement);
|
|
6061
|
+
// abortAllStreams が既に delete 済みだが、「clear = 全削除でも必ず restart 対象から
|
|
6062
|
+
// 外れる」不変条件を将来の abortAllStreams の変更から独立に保証するため明示的に呼ぶ。
|
|
6063
|
+
deleteActiveStateElement(stateElement);
|
|
6064
|
+
registryByStateElement.delete(stateElement);
|
|
6065
|
+
}
|
|
6066
|
+
|
|
6067
|
+
/**
|
|
6068
|
+
* stream/processStreamsDeclaration.ts
|
|
6069
|
+
*
|
|
6070
|
+
* `$streams: { <name>: { args?, source, fold?, initial? } }` 宣言マップを解析し、
|
|
6071
|
+
* IStreamEntry を構築して streamRegistry に一括登録する
|
|
6072
|
+
* (docs/state-streams-design.md §1-1 / §1-2 / §1-3)。
|
|
6073
|
+
*
|
|
6074
|
+
* - バリデーション(§1-2): 違反は raiseError。
|
|
6075
|
+
* - 名前はフラットなプロパティ名のみ(空文字 / `.`(DELIMITER)/ `*`(WILDCARD)/ 先頭 `$` を禁止)。
|
|
6076
|
+
* - Object.prototype の継承名(`__proto__` / `constructor` / `toString` 等)を禁止
|
|
6077
|
+
* (own key でなくても `in` 判定が真になり、実体化 skip + 起動時 Reflect.set の
|
|
6078
|
+
* 継承 setter 化 — `__proto__` は prototype 差し替え — を引き起こすため)。
|
|
6079
|
+
* - getter / setter として宣言済みのパスとの衝突を禁止(getterPaths / setterPaths を検査)。
|
|
6080
|
+
* - `source` は関数必須。`fold` は(あれば)関数。`fold` があるのに `initial` が無ければエラー
|
|
6081
|
+
* (reduce は initial 必須。`initial` の有無は in 演算子で判定)。`args` は(あれば)関数。
|
|
6082
|
+
* - fold 省略時は latest(`(_acc, chunk) => chunk`)を注入する(§0 決定レコード)。
|
|
6083
|
+
* - 値プロパティ実体化(§1-3): `state[name]` が未定義なら `initial`
|
|
6084
|
+
* (fold 無しなら undefined)でデータプロパティとして初期化する。
|
|
6085
|
+
* ユーザーが同名プロパティを先に宣言していた場合は上書きしない
|
|
6086
|
+
* (起動時の initial リセットは streamRuntime 側の責務)。
|
|
6087
|
+
* - 通知 dedup 台帳の prune(§4-3): 新宣言に存在しない名前の lastNotified エントリを
|
|
6088
|
+
* 削除する(台帳は stateElement 寿命 — 再 set 跨ぎ dedup が必要なのは同名のみ)。
|
|
6089
|
+
*
|
|
6090
|
+
* 呼び出しは stateElement.getterPaths / setterPaths の確定後であること
|
|
6091
|
+
* (State の `_state` セッターが getStateInfo の反映より後に呼ぶことで保証する)。
|
|
6092
|
+
*/
|
|
6093
|
+
/** fold 省略時に注入される既定 fold(latest = 最新チャンクで置換) */
|
|
6094
|
+
const latestFold = (_acc, chunk) => chunk;
|
|
6095
|
+
/** `$streams` 無し宣言の prune 用(旧宣言の全名前が残骸になる) */
|
|
6096
|
+
const NO_STREAM_NAMES = new Set();
|
|
6097
|
+
function processStreamsDeclaration(stateElement, state) {
|
|
6098
|
+
const declared = state[STATE_STREAMS_NAME];
|
|
6099
|
+
if (typeof declared === "undefined") {
|
|
6100
|
+
// $streams 無しの再 set でも旧宣言の名前は通知 dedup 台帳の残骸になるため prune する
|
|
6101
|
+
pruneLastNotified(stateElement, NO_STREAM_NAMES);
|
|
6102
|
+
return;
|
|
6103
|
+
}
|
|
6104
|
+
if (typeof declared !== "object" || declared === null) {
|
|
6105
|
+
raiseError(`${STATE_STREAMS_NAME} must be an object mapping stream names to stream definitions.`);
|
|
6106
|
+
}
|
|
6107
|
+
const entries = new Map();
|
|
6108
|
+
for (const [name, def] of Object.entries(declared)) {
|
|
6109
|
+
if (name.length === 0) {
|
|
6110
|
+
raiseError(`${STATE_STREAMS_NAME} entry name must be a non-empty string.`);
|
|
6111
|
+
}
|
|
6112
|
+
if (name.includes(DELIMITER)) {
|
|
6113
|
+
raiseError(`${STATE_STREAMS_NAME} entry "${name}" must be a flat property name ("${DELIMITER}" is not allowed).`);
|
|
6114
|
+
}
|
|
6115
|
+
if (name.includes(WILDCARD)) {
|
|
6116
|
+
raiseError(`${STATE_STREAMS_NAME} entry "${name}" must be a flat property name ("${WILDCARD}" is not allowed).`);
|
|
6117
|
+
}
|
|
6118
|
+
if (name.startsWith("$")) {
|
|
6119
|
+
raiseError(`${STATE_STREAMS_NAME} entry "${name}" must not start with "$" (reserved namespace).`);
|
|
6120
|
+
}
|
|
6121
|
+
// Object.prototype の継承名(__proto__ / constructor / toString 等)は一律拒否する。
|
|
6122
|
+
// own key でないのに `name in state` が真になるため実体化(§1-3)が skip され、
|
|
6123
|
+
// 起動時の initial リセット(Reflect.set)が継承 setter に化ける
|
|
6124
|
+
// (特に __proto__ は state の prototype を差し替える)ため、名前検査の防衛線で落とす(§1-2)。
|
|
6125
|
+
if (name in Object.prototype) {
|
|
6126
|
+
raiseError(`${STATE_STREAMS_NAME} entry "${name}" must not be a property name inherited from Object.prototype (e.g. "__proto__", "constructor").`);
|
|
6127
|
+
}
|
|
6128
|
+
if (stateElement.getterPaths.has(name)) {
|
|
6129
|
+
raiseError(`${STATE_STREAMS_NAME} entry "${name}" conflicts with a getter declared on the state.`);
|
|
6130
|
+
}
|
|
6131
|
+
if (stateElement.setterPaths.has(name)) {
|
|
6132
|
+
raiseError(`${STATE_STREAMS_NAME} entry "${name}" conflicts with a setter declared on the state.`);
|
|
6133
|
+
}
|
|
6134
|
+
if (typeof def !== "object" || def === null) {
|
|
6135
|
+
raiseError(`${STATE_STREAMS_NAME} entry "${name}" must be an object ({ args?, source, fold?, initial? }).`);
|
|
6136
|
+
}
|
|
6137
|
+
const definition = def;
|
|
6138
|
+
if (typeof definition.source !== "function") {
|
|
6139
|
+
raiseError(`${STATE_STREAMS_NAME} entry "${name}" source must be a function.`);
|
|
6140
|
+
}
|
|
6141
|
+
const hasFold = typeof definition.fold !== "undefined";
|
|
6142
|
+
if (hasFold && typeof definition.fold !== "function") {
|
|
6143
|
+
raiseError(`${STATE_STREAMS_NAME} entry "${name}" fold must be a function.`);
|
|
6144
|
+
}
|
|
6145
|
+
if (hasFold && !("initial" in definition)) {
|
|
6146
|
+
raiseError(`${STATE_STREAMS_NAME} entry "${name}" requires "initial" when fold is specified (reduce needs a seed value).`);
|
|
6147
|
+
}
|
|
6148
|
+
const hasArgs = typeof definition.args !== "undefined";
|
|
6149
|
+
if (hasArgs && typeof definition.args !== "function") {
|
|
6150
|
+
raiseError(`${STATE_STREAMS_NAME} entry "${name}" args must be a function.`);
|
|
6151
|
+
}
|
|
6152
|
+
const entry = {
|
|
6153
|
+
name,
|
|
6154
|
+
definition: {
|
|
6155
|
+
args: definition.args ?? null,
|
|
6156
|
+
source: definition.source,
|
|
6157
|
+
fold: definition.fold ?? latestFold,
|
|
6158
|
+
initial: definition.initial,
|
|
6159
|
+
},
|
|
6160
|
+
status: "idle",
|
|
6161
|
+
error: null,
|
|
6162
|
+
controller: null,
|
|
6163
|
+
depAddresses: new Set(),
|
|
6164
|
+
};
|
|
6165
|
+
// 値プロパティ実体化(§1-3): ユーザーが同名プロパティを先に宣言していたら上書きしない
|
|
6166
|
+
if (!(name in state)) {
|
|
6167
|
+
state[name] = entry.definition.initial;
|
|
6168
|
+
}
|
|
6169
|
+
entries.set(name, entry);
|
|
6170
|
+
}
|
|
6171
|
+
setStreamEntries(stateElement, entries);
|
|
6172
|
+
// 新宣言に存在しない名前の通知 dedup 台帳エントリを prune する
|
|
6173
|
+
// (同名は保持 = §4-3 の再 set 跨ぎ dedup 契約を維持。stream/lastNotified.ts 参照)
|
|
6174
|
+
pruneLastNotified(stateElement, new Set(entries.keys()));
|
|
6175
|
+
}
|
|
6176
|
+
|
|
6177
|
+
/**
|
|
6178
|
+
* stream/streamNamespace.ts
|
|
6179
|
+
*
|
|
6180
|
+
* `$streamStatus` / `$streamError` の read-only namespace proxy
|
|
6181
|
+
* (docs/state-streams-design.md §4-1 / §4-2)。commandNamespace と対称。
|
|
6182
|
+
*
|
|
6183
|
+
* - state element 単位で memo 化し、同一 stateElement なら同じ proxy が返る。
|
|
6184
|
+
* - 宣言された stream 名(`$streams` に列挙されたもの)のみ registry entry の
|
|
6185
|
+
* status / error を返す。宣言外の名前・Symbol キーは undefined
|
|
6186
|
+
* (`then` / `constructor` 等を内部機構が触っても throw しない寛容規約、
|
|
6187
|
+
* $command と同じ)。
|
|
6188
|
+
* - 値は memo しない: proxy は getStreamEntries を毎回読む thin gateway
|
|
6189
|
+
* (status / error は runtime が随時書き換えるため。registry entry が正本、§2-1)。
|
|
6190
|
+
* - set / deleteProperty は raiseError。setByAddress の親走査が namespace proxy に
|
|
6191
|
+
* 到達したときの Reflect.set もここで落ちる(書き込み防御 S11 の終端)。
|
|
6192
|
+
*/
|
|
6193
|
+
const statusNamespaceByStateElement = new WeakMap();
|
|
6194
|
+
const errorNamespaceByStateElement = new WeakMap();
|
|
6195
|
+
function createStreamNamespaceProxy(stateElement, namespaceName, pick) {
|
|
6196
|
+
return new Proxy(Object.create(null), {
|
|
6197
|
+
get(_target, prop) {
|
|
6198
|
+
if (typeof prop !== "string") {
|
|
6199
|
+
return undefined;
|
|
6200
|
+
}
|
|
6201
|
+
const entry = getStreamEntries(stateElement).get(prop);
|
|
6202
|
+
if (typeof entry === "undefined") {
|
|
6203
|
+
return undefined;
|
|
6204
|
+
}
|
|
6205
|
+
return pick(entry);
|
|
6206
|
+
},
|
|
6207
|
+
has(_target, prop) {
|
|
6208
|
+
return typeof prop === "string" && getStreamEntries(stateElement).has(prop);
|
|
6209
|
+
},
|
|
6210
|
+
ownKeys() {
|
|
6211
|
+
return Array.from(getStreamEntries(stateElement).keys());
|
|
6212
|
+
},
|
|
6213
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
6214
|
+
if (typeof prop !== "string") {
|
|
6215
|
+
return undefined;
|
|
6216
|
+
}
|
|
6217
|
+
const entry = getStreamEntries(stateElement).get(prop);
|
|
6218
|
+
if (typeof entry === "undefined") {
|
|
6219
|
+
return undefined;
|
|
6220
|
+
}
|
|
6221
|
+
return {
|
|
6222
|
+
configurable: true,
|
|
6223
|
+
enumerable: true,
|
|
6224
|
+
value: pick(entry),
|
|
6225
|
+
};
|
|
6226
|
+
},
|
|
6227
|
+
set() {
|
|
6228
|
+
raiseError(`${namespaceName} namespace is read-only; assigning to it is not allowed.`);
|
|
6229
|
+
},
|
|
6230
|
+
deleteProperty() {
|
|
6231
|
+
raiseError(`${namespaceName} namespace is read-only; deleting from it is not allowed.`);
|
|
6232
|
+
},
|
|
6233
|
+
});
|
|
6234
|
+
}
|
|
6235
|
+
function getStreamStatusNamespace(stateElement) {
|
|
6236
|
+
const cached = statusNamespaceByStateElement.get(stateElement);
|
|
6237
|
+
if (typeof cached !== "undefined") {
|
|
6238
|
+
return cached;
|
|
6239
|
+
}
|
|
6240
|
+
const proxy = createStreamNamespaceProxy(stateElement, STATE_STREAM_STATUS_NAMESPACE_NAME, (entry) => entry.status);
|
|
6241
|
+
statusNamespaceByStateElement.set(stateElement, proxy);
|
|
6242
|
+
return proxy;
|
|
6243
|
+
}
|
|
6244
|
+
function getStreamErrorNamespace(stateElement) {
|
|
6245
|
+
const cached = errorNamespaceByStateElement.get(stateElement);
|
|
6246
|
+
if (typeof cached !== "undefined") {
|
|
6247
|
+
return cached;
|
|
6248
|
+
}
|
|
6249
|
+
const proxy = createStreamNamespaceProxy(stateElement, STATE_STREAM_ERROR_NAMESPACE_NAME, (entry) => entry.error);
|
|
6250
|
+
errorNamespaceByStateElement.set(stateElement, proxy);
|
|
6251
|
+
return proxy;
|
|
6252
|
+
}
|
|
6253
|
+
/**
|
|
6254
|
+
* 両 namespace proxy の memo を破棄する(clearCommandNamespace と対称)。
|
|
6255
|
+
* disconnectedCallback と `_state` 再 set 時に呼ばれる。
|
|
6256
|
+
*/
|
|
6257
|
+
function clearStreamNamespace(stateElement) {
|
|
6258
|
+
statusNamespaceByStateElement.delete(stateElement);
|
|
6259
|
+
errorNamespaceByStateElement.delete(stateElement);
|
|
6260
|
+
}
|
|
6261
|
+
|
|
6262
|
+
const updateBatchListeners = new Set();
|
|
6263
|
+
/**
|
|
6264
|
+
* drain 終了リスナーを登録する。
|
|
6265
|
+
*/
|
|
6266
|
+
function registerUpdateBatchListener(listener) {
|
|
6267
|
+
updateBatchListeners.add(listener);
|
|
6268
|
+
}
|
|
6269
|
+
/**
|
|
6270
|
+
* 全リスナーに drain のバッチを通知する。
|
|
6271
|
+
* リスナーの throw は握りつぶさない(内部バグの隠蔽防止)。
|
|
6272
|
+
* stream 側リスナーが entry ごとに自前で try/catch する契約(設計書 §3-2)。
|
|
6273
|
+
*/
|
|
6274
|
+
function notifyUpdateBatchListeners(batch) {
|
|
6275
|
+
for (const listener of updateBatchListeners) {
|
|
6276
|
+
listener(batch);
|
|
6277
|
+
}
|
|
6278
|
+
}
|
|
6279
|
+
class Updater {
|
|
6280
|
+
_queueAbsoluteAddresses = [];
|
|
6281
|
+
constructor() {
|
|
6282
|
+
}
|
|
6283
|
+
enqueueAbsoluteAddress(absoluteAddress) {
|
|
6284
|
+
const requireStartProcess = this._queueAbsoluteAddresses.length === 0;
|
|
6285
|
+
this._queueAbsoluteAddresses.push(absoluteAddress);
|
|
6286
|
+
if (requireStartProcess) {
|
|
6287
|
+
queueMicrotask(() => {
|
|
6288
|
+
const absoluteAddresses = this._queueAbsoluteAddresses;
|
|
6289
|
+
this._queueAbsoluteAddresses = [];
|
|
6290
|
+
this._applyChange(absoluteAddresses);
|
|
6291
|
+
});
|
|
6292
|
+
}
|
|
6293
|
+
}
|
|
6294
|
+
// テスト用に公開
|
|
6295
|
+
testApplyChange(absoluteAddresses) {
|
|
6296
|
+
this._applyChange(absoluteAddresses);
|
|
6297
|
+
}
|
|
6298
|
+
_applyChange(absoluteAddresses) {
|
|
6299
|
+
// Note: AbsoluteStateAddress はキャッシュされているため、
|
|
6300
|
+
// 同一の (stateName, address) は同じインスタンスとなり、
|
|
6301
|
+
// Set による重複排除が正しく機能する
|
|
6302
|
+
const absoluteAddressSet = new Set(absoluteAddresses);
|
|
6303
|
+
const processBindings = [];
|
|
6304
|
+
for (const absoluteAddress of absoluteAddressSet) {
|
|
6305
|
+
// peek: バインディングの無いアドレス(リスト置換で enqueue される中間
|
|
6306
|
+
// アドレス等)に空 Set を生成・蓄積しない
|
|
6307
|
+
const bindings = peekBindingSetByAbsoluteStateAddress(absoluteAddress);
|
|
6308
|
+
if (bindings === undefined) {
|
|
6309
|
+
continue;
|
|
6310
|
+
}
|
|
6311
|
+
for (const binding of bindings) {
|
|
6312
|
+
if (binding.replaceNode.isConnected === false) {
|
|
6313
|
+
// 切断されているバインディングは無視
|
|
6314
|
+
continue;
|
|
6315
|
+
}
|
|
6316
|
+
processBindings.push(binding);
|
|
6317
|
+
}
|
|
6318
|
+
}
|
|
6319
|
+
applyChangeFromBindings(processBindings);
|
|
6320
|
+
// drain 終了フック: binding 適用後に dedup 済みバッチを通知する(設計書 §3-2)。
|
|
6321
|
+
// testApplyChange も同じ _applyChange を通るため、テストから同期に駆動できる。
|
|
6322
|
+
notifyUpdateBatchListeners(absoluteAddressSet);
|
|
6323
|
+
}
|
|
6324
|
+
}
|
|
6325
|
+
const updater = new Updater();
|
|
6326
|
+
function getUpdater() {
|
|
6327
|
+
return updater;
|
|
6328
|
+
}
|
|
6329
|
+
|
|
6330
|
+
/**
|
|
6331
|
+
* stream/argsTrace.ts
|
|
6332
|
+
*
|
|
6333
|
+
* `$streams` の args トレース(依存捕捉、docs/state-streams-design.md §3-1)。
|
|
6334
|
+
*
|
|
6335
|
+
* - モジュールスコープの collector を立てて readonly proxy 上で args を評価し、
|
|
6336
|
+
* getByAddress を通った読みを絶対アドレス(IAbsoluteStateAddress)として捕捉する。
|
|
6337
|
+
* AbsolutePathInfo / AbsoluteStateAddress は両方キャッシュ済みのため、捕捉した
|
|
6338
|
+
* アドレスは drain バッチと Set.has のインスタンス同一性で O(1) 照合できる(§2-1)。
|
|
6339
|
+
* - collectStreamDependency は getByAddress のホットパスから毎読み呼ばれるため、
|
|
6340
|
+
* collector === null なら即 return し、それ以外の計算を一切しない。
|
|
6341
|
+
* - 起動・restart のたびに traceArgs が呼ばれ、成功時は entry.depAddresses を
|
|
6342
|
+
* 丸ごと置換する(per-run の動的再捕捉)。失敗時は前回成功 run の検証済み
|
|
6343
|
+
* 捕捉を保持する(§2-2 の「error からも依存変化で restart」を保つ)。
|
|
6344
|
+
* - lastNotified.ts と同じく import 循環回避のための小モジュール
|
|
6345
|
+
* (getByAddress → argsTrace ← streamRuntime の一方向依存に保つ)。
|
|
6346
|
+
*/
|
|
6347
|
+
/** トレース中のみ非 null。getByAddress を通った読みの絶対アドレスが溜まる。 */
|
|
6348
|
+
let collector = null;
|
|
6349
|
+
/**
|
|
6350
|
+
* getByAddress の入口(checkDependency 直後)から毎読み呼ばれるフック。
|
|
6351
|
+
* トレース外(collector === null)では何もしない。
|
|
6352
|
+
*/
|
|
6353
|
+
function collectStreamDependency(stateElement, address) {
|
|
6354
|
+
if (collector === null) {
|
|
6355
|
+
return;
|
|
6356
|
+
}
|
|
6357
|
+
const absolutePathInfo = getAbsolutePathInfo(stateElement, address.pathInfo);
|
|
6358
|
+
collector.add(createAbsoluteStateAddress(absolutePathInfo, address.listIndex));
|
|
6359
|
+
}
|
|
6360
|
+
/**
|
|
6361
|
+
* args を readonly proxy で同期評価し、読まれたパスを entry.depAddresses に
|
|
6362
|
+
* 丸ごと置換で再捕捉する(§3-1)。評価値(source の第 1 引数になる)を返す。
|
|
6363
|
+
*
|
|
6364
|
+
* - args === null(宣言で省略)なら depAddresses を clear して undefined
|
|
6365
|
+
* (依存なし = 起動後 restart しない)。
|
|
6366
|
+
* - 検査(違反は raiseError):
|
|
6367
|
+
* (a) 評価値が Promise(同期契約違反)
|
|
6368
|
+
* (b) 自己依存 — `<name>` / `$streamStatus.<name>` / `$streamError.<name>` の読み
|
|
6369
|
+
* (restart の自己書き込みで再発火する無限ループ、S8)
|
|
6370
|
+
* (c) wildcard を含むパスの読み(`$getAll` 等も同様。第 1 段スコープ外)
|
|
6371
|
+
* - 失敗時(args のユーザー例外・検査違反)は今回の捕捉(captured)を採用せず
|
|
6372
|
+
* 伝播し、entry.depAddresses には**前回成功 run の検証済み捕捉を保持する**。
|
|
6373
|
+
* これにより drain リスナーが throw を error 経路に正規化したあとも、依存の
|
|
6374
|
+
* 書き込みで再試行できる(§2-2「done / error からも依存変化で restart」——
|
|
6375
|
+
* 一時的な args throw で stream が恒久固着しない)。ループ安全性:
|
|
6376
|
+
* 保持されるのは前回**成功** run の捕捉のみ(自己依存・wildcard 検査済み)で
|
|
6377
|
+
* 自分の `<name>` / `$streamStatus.<name>` / `$streamError.<name>` を含み得ず、
|
|
6378
|
+
* traceArgs throw 時の startStream は initial リセットに到達しないため、
|
|
6379
|
+
* error 正規化の書き込みが保持 deps に再 hit することはない。再試行は依存
|
|
6380
|
+
* 書き込み 1 回につき高々 1 回で有界。未検査の captured を採用しないことが
|
|
6381
|
+
* ループ防止の要件であり、前回検証済み捕捉の保持はそれを侵さない。
|
|
6382
|
+
* - collector は finally で必ず復元する(例外・再入安全。ネスト評価は想定しないが
|
|
6383
|
+
* 防御的に「前の collector を復元」の形にしておく — コストは同等)。
|
|
6384
|
+
*/
|
|
6385
|
+
function traceArgs(stateElement, entry) {
|
|
6386
|
+
const argsFn = entry.definition.args;
|
|
6387
|
+
if (argsFn === null) {
|
|
6388
|
+
entry.depAddresses.clear();
|
|
6389
|
+
return undefined;
|
|
6390
|
+
}
|
|
6391
|
+
const previousCollector = collector;
|
|
6392
|
+
const captured = new Set();
|
|
6393
|
+
collector = captured;
|
|
6394
|
+
let argsValue = undefined;
|
|
6395
|
+
try {
|
|
6396
|
+
stateElement.createState("readonly", (state) => {
|
|
6397
|
+
argsValue = argsFn(state);
|
|
6398
|
+
});
|
|
6399
|
+
}
|
|
6400
|
+
finally {
|
|
6401
|
+
// args のユーザー例外時は captured を採用せずそのまま伝播する
|
|
6402
|
+
// (entry.depAddresses は前回成功 run の検証済み捕捉を保持)
|
|
6403
|
+
collector = previousCollector;
|
|
6404
|
+
}
|
|
6405
|
+
if (argsValue instanceof Promise) {
|
|
6406
|
+
raiseError(`${STATE_STREAMS_NAME} entry "${entry.name}" args must be synchronous (it returned a Promise).`);
|
|
6407
|
+
}
|
|
6408
|
+
const selfStatusPath = `${STATE_STREAM_STATUS_NAMESPACE_NAME}${DELIMITER}${entry.name}`;
|
|
6409
|
+
const selfErrorPath = `${STATE_STREAM_ERROR_NAMESPACE_NAME}${DELIMITER}${entry.name}`;
|
|
6410
|
+
for (const dep of captured) {
|
|
6411
|
+
const pathInfo = dep.absolutePathInfo.pathInfo;
|
|
6412
|
+
if (dep.absolutePathInfo.stateElement === stateElement &&
|
|
6413
|
+
(pathInfo.path === entry.name || pathInfo.path === selfStatusPath || pathInfo.path === selfErrorPath)) {
|
|
6414
|
+
raiseError(`${STATE_STREAMS_NAME} entry "${entry.name}" args must not read the stream itself ("${pathInfo.path}"): a self-dependency would restart the stream on its own writes (infinite loop).`);
|
|
6415
|
+
}
|
|
6416
|
+
if (pathInfo.wildcardCount > 0) {
|
|
6417
|
+
raiseError(`${STATE_STREAMS_NAME} entry "${entry.name}" args must not read wildcard paths ("${pathInfo.path}"): wildcard dependencies are out of scope.`);
|
|
6418
|
+
}
|
|
6419
|
+
}
|
|
6420
|
+
entry.depAddresses = captured;
|
|
6421
|
+
return argsValue;
|
|
6422
|
+
}
|
|
6423
|
+
|
|
6424
|
+
/**
|
|
6425
|
+
* stream/consumeSource.ts
|
|
6426
|
+
*
|
|
6427
|
+
* `$streams` のチャンク消費ループ(docs/state-streams-design.md §3-3)。
|
|
6428
|
+
* packages/signals/src/streamResource.ts の consume / iterate /
|
|
6429
|
+
* readableToAsyncIterable の移植(パッケージ間依存は持たない自己完結原則)。
|
|
6430
|
+
*
|
|
6431
|
+
* 唯一の構造差分は状態書き込みの IConsumeSink への委譲:
|
|
6432
|
+
* value.set(fold(value.peek(), chunk)) → sink.fold(chunk)
|
|
6433
|
+
* status.set("done") → sink.done()
|
|
6434
|
+
* error.set(e) + status.set("error") → sink.fail(e)
|
|
6435
|
+
*
|
|
6436
|
+
* sink.fold() が throw した場合(fold throw)もループ内の throw として
|
|
6437
|
+
* 既存の catch に流れ、signal.aborted なら return、でなければ sink.fail(e)。
|
|
6438
|
+
* consumeSource 自体は fold throw と source throw を区別しない
|
|
6439
|
+
* (producer の掃除 = controller.abort() は呼び出し側 runtime が fail 内で行う)。
|
|
6440
|
+
* consumeSource は reject しない(全経路 catch 済み)。
|
|
6441
|
+
*
|
|
6442
|
+
* ---------------------------------------------------------------------------
|
|
6443
|
+
* 以下、移植元モジュールヘッダの契約(原文英語のまま維持):
|
|
6444
|
+
*
|
|
6445
|
+
* CONTRACT (cooperative cancellation — STRONG REQUIREMENT): the `source` MUST honor
|
|
6446
|
+
* the `AbortSignal` it is given. Honoring it is what drives switchMap restart/dispose;
|
|
6447
|
+
* a source that ignores it cannot be reliably cancelled.
|
|
6448
|
+
*
|
|
6449
|
+
* Rescue levels on abort:
|
|
6450
|
+
* - ReadableStream: FULLY rescued. A parked read() is force-unwound via
|
|
6451
|
+
* reader.cancel(), which both releases the underlying source and settles the
|
|
6452
|
+
* pending read() so the loop unwinds.
|
|
6453
|
+
* - AsyncIterable / async generator: PARTIALLY rescued. On abort we call
|
|
6454
|
+
* iterator.return() to trigger the generator's finally/cleanup. But a parked
|
|
6455
|
+
* `await` (the producer stalling before its next yield while IGNORING `signal`)
|
|
6456
|
+
* cannot be force-unwound from outside — return() only takes effect when the
|
|
6457
|
+
* generator next resumes. So a source that parks forever and never observes
|
|
6458
|
+
* `signal` still leaks its consume task. Honor `signal` to bound this.
|
|
6459
|
+
* The `if (signal.aborted) return` check only runs after a chunk arrives, not while
|
|
6460
|
+
* parked — it drops stale chunks but is not, by itself, a cancellation mechanism.
|
|
6461
|
+
*/
|
|
6462
|
+
async function consumeSource(source, args, signal, sink) {
|
|
6463
|
+
// Obtain the iterator EXPLICITLY (not via `for await`'s implicit one) so abort can
|
|
6464
|
+
// call `iterator.return()` to trigger an AsyncIterable / async generator's
|
|
6465
|
+
// `finally`/cleanup. A `for await` only calls `.return()` when the loop itself exits;
|
|
6466
|
+
// if the producer is PARKED (awaiting before the next yield while ignoring `signal`),
|
|
6467
|
+
// the loop never advances, so the implicit `.return()` never runs and the task leaks
|
|
6468
|
+
// past restart/dispose. Calling `.return()` on abort is the PARTIAL rescue: the
|
|
6469
|
+
// parked `await` cannot be force-unwound from outside, but once the generator resumes
|
|
6470
|
+
// (its next tick), `.return()` makes it run its `finally` and stop — recovering the
|
|
6471
|
+
// common "generator wakes up after abort" case. The ReadableStream path is fully
|
|
6472
|
+
// rescued via `reader.cancel()` (see `readableToAsyncIterable`).
|
|
6473
|
+
let iterator = null;
|
|
6474
|
+
// Guard against returning the SAME iterator twice. `onAbort` is reachable two ways:
|
|
6475
|
+
// the abort listener, and the explicit call below when abort raced the
|
|
6476
|
+
// `await source(...)`. The guard keys on the iterator instance (not a plain "ran"
|
|
6477
|
+
// flag): the listener firing with iterator still null must NOT consume the single
|
|
6478
|
+
// real cleanup that the explicit call performs once the iterator exists. So we only
|
|
6479
|
+
// mark an iterator returned once we have actually called `.return()` on it.
|
|
6480
|
+
let returned = null;
|
|
6481
|
+
const onAbort = () => {
|
|
6482
|
+
if (!iterator || iterator === returned) {
|
|
6483
|
+
return; // nothing to release yet, or already released this iterator
|
|
6484
|
+
}
|
|
6485
|
+
returned = iterator;
|
|
6486
|
+
// Fire the iterator's cleanup. Swallow any throw/rejection from `.return()` — we
|
|
6487
|
+
// are tearing down; a producer that rejects on return must not surface here.
|
|
6488
|
+
try {
|
|
6489
|
+
void iterator.return?.()?.then?.(undefined, () => { });
|
|
6490
|
+
}
|
|
6491
|
+
catch {
|
|
6492
|
+
// `.return()` threw synchronously while tearing down — ignore.
|
|
6493
|
+
}
|
|
6494
|
+
};
|
|
6495
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
6496
|
+
try {
|
|
6497
|
+
const produced = await source(args, signal);
|
|
6498
|
+
iterator = iterate(produced, signal)[Symbol.asyncIterator]();
|
|
6499
|
+
if (signal.aborted) {
|
|
6500
|
+
// Aborted while awaiting the source: the abort listener already ran (iterator
|
|
6501
|
+
// was still null then), so explicitly release the just-produced iterator now —
|
|
6502
|
+
// this fires a generator's finally / a ReadableStream's cancel for the
|
|
6503
|
+
// resource we created but will never iterate.
|
|
6504
|
+
onAbort();
|
|
6505
|
+
return;
|
|
6506
|
+
}
|
|
6507
|
+
for (;;) {
|
|
6508
|
+
const result = await iterator.next();
|
|
6509
|
+
if (result.done) {
|
|
6510
|
+
break;
|
|
6511
|
+
}
|
|
6512
|
+
if (signal.aborted) {
|
|
6513
|
+
return; // stale chunk from a superseded/disposed run — drop it
|
|
6514
|
+
}
|
|
6515
|
+
sink.fold(result.value);
|
|
6516
|
+
}
|
|
6517
|
+
if (signal.aborted) {
|
|
6518
|
+
return; // stream ended but this run was aborted — don't mark done
|
|
6519
|
+
}
|
|
6520
|
+
sink.done();
|
|
6521
|
+
}
|
|
6522
|
+
catch (e) {
|
|
6523
|
+
if (signal.aborted) {
|
|
6524
|
+
return; // an abort that surfaced as a throw is not an error
|
|
6525
|
+
}
|
|
6526
|
+
sink.fail(e); // keep the last folded value (do not reset)
|
|
6527
|
+
}
|
|
6528
|
+
finally {
|
|
6529
|
+
signal.removeEventListener("abort", onAbort);
|
|
6530
|
+
}
|
|
6531
|
+
}
|
|
6532
|
+
function iterate(produced, signal) {
|
|
6533
|
+
// Optional chaining: a null/undefined source return value must fall through to the
|
|
6534
|
+
// explicit TypeError below (symmetric with the `?.` on the getReader probe), not
|
|
6535
|
+
// throw an opaque "Cannot read properties of null" from this property access.
|
|
6536
|
+
if (typeof produced?.[Symbol.asyncIterator] === "function") {
|
|
6537
|
+
return produced;
|
|
6538
|
+
}
|
|
6539
|
+
// Not async-iterable: must be a ReadableStream (read via getReader). Validate so
|
|
6540
|
+
// a wrong source value yields a clear error instead of an opaque "getReader is
|
|
6541
|
+
// not a function" from inside the generator.
|
|
6542
|
+
if (typeof produced?.getReader !== "function") {
|
|
6543
|
+
throw new TypeError("[@wcstack/state] $streams: source must return an AsyncIterable or a ReadableStream (got neither).");
|
|
6544
|
+
}
|
|
6545
|
+
return readableToAsyncIterable(produced, signal);
|
|
6546
|
+
}
|
|
6547
|
+
async function* readableToAsyncIterable(stream, signal) {
|
|
6548
|
+
const reader = stream.getReader();
|
|
6549
|
+
// A ReadableStream read() does NOT observe an AbortSignal on its own. Without
|
|
6550
|
+
// this, a switchMap restart / dispose leaves the previous reader parked in a
|
|
6551
|
+
// pending read() forever, leaking the underlying source. Cancelling on abort
|
|
6552
|
+
// both releases the source AND settles the pending read() so the for-await
|
|
6553
|
+
// unwinds and the finally below can release the lock. Abort is the only
|
|
6554
|
+
// early-exit path for this generator (the consumer never calls .return()
|
|
6555
|
+
// without aborting), so this is the sole place a non-drained stream is cancelled.
|
|
6556
|
+
const onAbort = () => {
|
|
6557
|
+
void reader.cancel().catch(() => { }); // tearing down; swallow a rejected cancel
|
|
6558
|
+
};
|
|
6559
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
6560
|
+
try {
|
|
6561
|
+
for (;;) {
|
|
6562
|
+
const { done, value } = await reader.read();
|
|
6563
|
+
if (done) {
|
|
6564
|
+
return;
|
|
6565
|
+
}
|
|
6566
|
+
yield value;
|
|
6567
|
+
}
|
|
6568
|
+
}
|
|
6569
|
+
finally {
|
|
6570
|
+
signal.removeEventListener("abort", onAbort);
|
|
6571
|
+
reader.releaseLock();
|
|
6572
|
+
}
|
|
6573
|
+
}
|
|
6574
|
+
|
|
6575
|
+
/**
|
|
6576
|
+
* stream/streamRuntime.ts
|
|
6577
|
+
*
|
|
6578
|
+
* `$streams` の起動・チャンク反映・status 遷移(docs/state-streams-design.md
|
|
6579
|
+
* §2-2 / §3-3 / §4-3)。
|
|
6580
|
+
*
|
|
6581
|
+
* スコープ:
|
|
6582
|
+
* - eager 起動(startStreams)と start = restart の共通手順(startStream)。
|
|
6583
|
+
* - args は traceArgs(stream/argsTrace.ts)で readonly proxy 評価と同時に依存を
|
|
6584
|
+
* per-run 再捕捉する(§3-1)。
|
|
6585
|
+
* - 依存駆動 restart(§3-2): モジュール初期化時に updater の drain 終了リスナーを
|
|
6586
|
+
* 1 つ登録し(restartStreamsOnUpdateBatch)、起動中 stateElement
|
|
6587
|
+
* (stream/activeStateElements.ts — startStreams で add・abortAllStreams /
|
|
6588
|
+
* clearStreamRegistry で delete)の各 entry について depAddresses と batch を
|
|
6589
|
+
* 交差させ、hit した entry を restart する。
|
|
6590
|
+
*
|
|
6591
|
+
* 切断後の後始末について(不変条件):
|
|
6592
|
+
* - disconnect(abortAllStreams)は registry entry を直接ミューテーションして
|
|
6593
|
+
* idle に戻す($postUpdate は呼ばない — 切断済みで binding 更新は不要かつ
|
|
6594
|
+
* rootNode が無い)。
|
|
6595
|
+
* - abort 済み run の sink コールバック(fold / done / fail)は consumeSource の
|
|
6596
|
+
* stale-drop(全経路の signal.aborted チェック)が createState 到達前に
|
|
6597
|
+
* 落とすため、runtime 側に切断後ガードは不要。
|
|
6598
|
+
* 「runtime が createState を呼ぶのは自分の controller が生きている間だけ」が
|
|
6599
|
+
* この 2 つの組み合わせで常に保たれる。
|
|
6600
|
+
*/
|
|
6601
|
+
/**
|
|
6602
|
+
* 登録済みの全 stream を起動する(eager 起動、設計書 §2-3)。
|
|
6603
|
+
* State.connectedCallback($connectedCallback 完了後)と接続中の `_state` 再 set
|
|
6604
|
+
* から呼ばれる想定。
|
|
6605
|
+
*
|
|
6606
|
+
* 同時に依存駆動 restart(§3-2)の対象として activeStateElements に登録する
|
|
6607
|
+
* (delete 側は abortAllStreams / clearStreamRegistry —
|
|
6608
|
+
* stream/activeStateElements.ts のリーク防止不変条件を参照)。
|
|
6609
|
+
* eager 起動の throw(args のユーザー例外等)はここでは正規化せず loud fail のまま
|
|
6610
|
+
* (既存の $connectedCallback と同じ扱い。正規化は drain リスナー側の restart のみ)。
|
|
6611
|
+
*/
|
|
6612
|
+
function startStreams(stateElement) {
|
|
6613
|
+
const entries = getStreamEntries(stateElement);
|
|
6614
|
+
if (entries.size === 0) {
|
|
6615
|
+
return;
|
|
6616
|
+
}
|
|
6617
|
+
addActiveStateElement(stateElement);
|
|
6618
|
+
for (const entry of entries.values()) {
|
|
6619
|
+
startStream(stateElement, entry);
|
|
6620
|
+
}
|
|
6621
|
+
}
|
|
6622
|
+
/**
|
|
6623
|
+
* stream を起動する。start = restart の共通手順(設計書 §2-2):
|
|
6624
|
+
*
|
|
6625
|
+
* 1. 旧 run を abort(restart 時)→ 新 AbortController
|
|
6626
|
+
* 2. traceArgs で args を readonly proxy 評価し依存を丸ごと再捕捉
|
|
6627
|
+
* (Promise / 自己依存 / wildcard 読みは raiseError、§3-1)
|
|
6628
|
+
* 3. 値を initial にリセット(起動 = 最初の run も restart と同一セマンティクス、§1-3)
|
|
6629
|
+
* 4. status="active"・error=null を反映
|
|
6630
|
+
* 5. consumeSource で消費開始
|
|
6631
|
+
*/
|
|
6632
|
+
function startStream(stateElement, entry) {
|
|
6633
|
+
entry.controller?.abort();
|
|
6634
|
+
const controller = new AbortController();
|
|
6635
|
+
entry.controller = controller;
|
|
6636
|
+
// args 評価 + 依存の per-run 再捕捉(args === null なら depAddresses を clear して
|
|
6637
|
+
// undefined。Promise / 自己依存 / wildcard 読みは raiseError、§3-1)
|
|
6638
|
+
const argsValue = traceArgs(stateElement, entry);
|
|
6639
|
+
// 値リセット: setByAddress を通すことで updater coalesce・sameValueGuard・
|
|
6640
|
+
// walkDependency(stream 値に依存する computed の dirty 化)がすべて乗る(§3-3)
|
|
6641
|
+
stateElement.createState("writable", (state) => {
|
|
6642
|
+
state[entry.name] = entry.definition.initial;
|
|
6643
|
+
});
|
|
6644
|
+
updateStreamStatus(stateElement, entry, "active", null);
|
|
6645
|
+
const definition = entry.definition;
|
|
6646
|
+
const sink = {
|
|
6647
|
+
fold(chunk) {
|
|
6648
|
+
// fold の throw はそのまま伝播させる(consumeSource が fail 経路に回す)
|
|
6649
|
+
stateElement.createState("writable", (state) => {
|
|
6650
|
+
state[entry.name] = definition.fold(state[entry.name], chunk);
|
|
6651
|
+
});
|
|
6652
|
+
},
|
|
6653
|
+
done() {
|
|
6654
|
+
updateStreamStatus(stateElement, entry, "done", null);
|
|
6655
|
+
},
|
|
6656
|
+
fail(error) {
|
|
6657
|
+
// 値は直前の fold 結果を保持(リセットしない)
|
|
6658
|
+
updateStreamStatus(stateElement, entry, "error", error);
|
|
6659
|
+
// fold-throw 時の producer 掃除(iterator.return() / reader.cancel() を発火)。
|
|
6660
|
+
// source-throw 時は producer が既に終了しているので abort は無害(§3-3)。
|
|
6661
|
+
controller.abort();
|
|
6662
|
+
},
|
|
6663
|
+
};
|
|
6664
|
+
void consumeSource(definition.source, argsValue, controller.signal, sink);
|
|
6665
|
+
}
|
|
6666
|
+
/**
|
|
6667
|
+
* status / error の反映ヘルパ(設計書 §4-3)。
|
|
6668
|
+
*
|
|
6669
|
+
* - registry entry が正本。常に最新値へ書き換える。
|
|
6670
|
+
* - 「最後に通知した観測値」(stream/lastNotified.ts — 再 set・再接続を跨いで
|
|
6671
|
+
* stateElement の寿命で生存する台帳)から変化した項目に対応する名前空間パス
|
|
6672
|
+
* (`$streamStatus.<name>` / `$streamError.<name>`)だけを writable proxy の
|
|
6673
|
+
* $postUpdate で通知する(updater enqueue + walkDependency)。
|
|
6674
|
+
* - 両方不変なら通知しない(名前空間パスは setByAddress を通らないため
|
|
6675
|
+
* sameValueGuard が効かず、同等の same-value 判定を runtime 側が持つ)。
|
|
6676
|
+
* abortAllStreams の無通知ミューテーションで台帳が invalidate されている場合は
|
|
6677
|
+
* 同値扱いにならず必ず通知される(再接続ウィンドウ内の fresh 読みが描画した
|
|
6678
|
+
* idle の恒久陳腐化を防ぐ、§4-3)。
|
|
6679
|
+
*/
|
|
6680
|
+
function updateStreamStatus(stateElement, entry, status, error) {
|
|
6681
|
+
entry.status = status;
|
|
6682
|
+
entry.error = error;
|
|
6683
|
+
const last = getLastNotified(stateElement, entry.name);
|
|
6684
|
+
const statusChanged = last.status !== status;
|
|
6685
|
+
const errorChanged = !Object.is(last.error, error);
|
|
6686
|
+
if (!statusChanged && !errorChanged) {
|
|
6687
|
+
return;
|
|
6688
|
+
}
|
|
6689
|
+
setLastNotified(stateElement, entry.name, status, error);
|
|
6690
|
+
stateElement.createState("writable", (state) => {
|
|
6691
|
+
if (statusChanged) {
|
|
6692
|
+
state.$postUpdate(`${STATE_STREAM_STATUS_NAMESPACE_NAME}${DELIMITER}${entry.name}`);
|
|
6693
|
+
}
|
|
6694
|
+
if (errorChanged) {
|
|
6695
|
+
state.$postUpdate(`${STATE_STREAM_ERROR_NAMESPACE_NAME}${DELIMITER}${entry.name}`);
|
|
6696
|
+
}
|
|
6697
|
+
});
|
|
6698
|
+
}
|
|
6699
|
+
/**
|
|
6700
|
+
* 依存駆動 restart の drain リスナー(設計書 §3-2)。
|
|
6701
|
+
* モジュール初期化時に registerUpdateBatchListener で 1 つだけ登録される。
|
|
6702
|
+
*
|
|
6703
|
+
* - 起動中の各 stateElement の各 entry について、depAddresses と batch の交差を
|
|
6704
|
+
* Set.has のインスタンス同一性で判定する(小さい方 = depAddresses を回して
|
|
6705
|
+
* batch.has(dep)。AbsoluteStateAddress はキャッシュにより同一 (stateName, path,
|
|
6706
|
+
* listIndex) が同一インスタンス、§2-1)。args なし(depAddresses 空)の entry は
|
|
6707
|
+
* 自然にスキップされる。
|
|
6708
|
+
* - status は問わず restart する(done / error からも依存の叩き直しで再試行、§2-2)。
|
|
6709
|
+
* - hit は収集してから一括で restart する(イテレーション中の registry 変更を避ける。
|
|
6710
|
+
* entry ごとに最初の hit で break するため「1 drain につき 1 entry 最大 1 restart」
|
|
6711
|
+
* もここで自然に成立する — 同一 tick 内の複数依存書き込みは 1 restart に畳まれる)。
|
|
6712
|
+
* - hits の実行時にも active + entry identity を再チェックする: 先行 restart の
|
|
6713
|
+
* source / args は consumeSource / traceArgs の同期プレフィックスで同期実行される
|
|
6714
|
+
* ため、そこで (a) 他の stateElement(や自分自身のホスト)の同期切断、(b) 同一要素の
|
|
6715
|
+
* _state 同期再 set(clearStreamRegistry → startStreams で Set に再 add される)が
|
|
6716
|
+
* 起こり得る。(a) は切断済み要素への startStream が rootNode 不在で throw する経路、
|
|
6717
|
+
* (b) は registry から置換済みの旧 entry を restart して到達不能な孤児 consume run を
|
|
6718
|
+
* リークする経路(§3-2「未接続の stateElement の entry は restart しない」・
|
|
6719
|
+
* §5-1「切断後は idle」に違反)で、いずれも「entry が現行 registry の live entry で
|
|
6720
|
+
* あること」の再検証で skip する。startStream **実行中**の自己切断・再 set は事前
|
|
6721
|
+
* チェックではガードできないため、catch 側でも同じ再検証を行ってから error に
|
|
6722
|
+
* 正規化する(切断済みでの正規化は createState が再 throw して drain リスナー外へ
|
|
6723
|
+
* 漏れ、後続 hits の restart を巻き添えにするため)。
|
|
6724
|
+
* - restart(startStream)は entry ごとに try/catch し、throw(args のユーザー例外・
|
|
6725
|
+
* Promise 同期契約違反等)は controller.abort() → status="error"・$streamError 格納
|
|
6726
|
+
* に正規化する(§3-2 規範 3)。updater の drain を壊さず、他 entry の restart も
|
|
6727
|
+
* 継続する。eager 起動(connect 時の startStreams)の throw は従来どおり loud fail。
|
|
6728
|
+
* - restart 内の書き込み(initial リセット・status 通知)は updater への enqueue のみで
|
|
6729
|
+
* 新しい microtask バッチを作る(drain 再入ではない)。自己依存は traceArgs が
|
|
6730
|
+
* 宣言時に raiseError で検出するため、restart 書き込みが自分の依存に再 hit する
|
|
6731
|
+
* ループは起きない(§3-1)。
|
|
6732
|
+
*/
|
|
6733
|
+
function restartStreamsOnUpdateBatch(batch) {
|
|
6734
|
+
const activeStateElements = getActiveStateElements();
|
|
6735
|
+
if (activeStateElements.size === 0) {
|
|
6736
|
+
// stream 未使用アプリの drain に配列・イテレータ割り当てのコストを載せない
|
|
6737
|
+
return;
|
|
6738
|
+
}
|
|
6739
|
+
const hits = [];
|
|
6740
|
+
for (const stateElement of activeStateElements) {
|
|
6741
|
+
for (const entry of getStreamEntries(stateElement).values()) {
|
|
6742
|
+
for (const dep of entry.depAddresses) {
|
|
6743
|
+
if (batch.has(dep)) {
|
|
6744
|
+
hits.push({ stateElement, entry });
|
|
6745
|
+
break;
|
|
6746
|
+
}
|
|
6747
|
+
}
|
|
6748
|
+
}
|
|
6749
|
+
}
|
|
6750
|
+
for (const { stateElement, entry } of hits) {
|
|
6751
|
+
// 先行 restart の source / args 同期実行は他要素の切断や同一要素の _state 同期再 set を
|
|
6752
|
+
// 行い得るため、実行時に再チェックする(live な Set / registry ビューで即時反映):
|
|
6753
|
+
// - 切断済み要素は skip(§3-2「未接続の stateElement の entry は restart しない」)
|
|
6754
|
+
// - entry が現行 registry のものでなければ skip — 同期再 set で置換された旧 entry を
|
|
6755
|
+
// restart すると、registry から到達不能なため abortAllStreams でも止められない
|
|
6756
|
+
// 孤児 consume run がリークする
|
|
6757
|
+
if (!activeStateElements.has(stateElement) ||
|
|
6758
|
+
getStreamEntries(stateElement).get(entry.name) !== entry) {
|
|
6759
|
+
continue;
|
|
6760
|
+
}
|
|
6761
|
+
try {
|
|
6762
|
+
startStream(stateElement, entry);
|
|
6763
|
+
}
|
|
6764
|
+
catch (e) {
|
|
6765
|
+
entry.controller?.abort();
|
|
6766
|
+
// startStream 実行中(args / source の同期プレフィックス)の自己切断・同期再 set は
|
|
6767
|
+
// 上の再チェックではガードできない。切断済みだと updateStreamStatus の createState が
|
|
6768
|
+
// rootNode 不在で再 throw して drain リスナー外へ漏れる(後続 hits の restart を
|
|
6769
|
+
// 巻き添えにする)ため、entry がまだ現行の live entry である場合のみ error に
|
|
6770
|
+
// 正規化する(切断済みなら abortAllStreams が idle に戻し済み。§3-2 規範 3 / §5-1)。
|
|
6771
|
+
if (activeStateElements.has(stateElement) &&
|
|
6772
|
+
getStreamEntries(stateElement).get(entry.name) === entry) {
|
|
6773
|
+
updateStreamStatus(stateElement, entry, "error", e);
|
|
6774
|
+
}
|
|
6775
|
+
}
|
|
6776
|
+
}
|
|
6777
|
+
}
|
|
6778
|
+
registerUpdateBatchListener(restartStreamsOnUpdateBatch);
|
|
6779
|
+
|
|
6780
|
+
function getterFn(name) {
|
|
6781
|
+
return function () {
|
|
6782
|
+
const stateEl = this.stateElement;
|
|
6783
|
+
if (!stateEl)
|
|
6784
|
+
return undefined;
|
|
6785
|
+
let value;
|
|
6786
|
+
try {
|
|
6787
|
+
stateEl.createState("readonly", (state) => {
|
|
6788
|
+
value = state[name];
|
|
6789
|
+
});
|
|
6790
|
+
}
|
|
6791
|
+
catch (e) {
|
|
6792
|
+
console.warn(`[@wcstack/state] DCC getter "${name}" failed:`, e);
|
|
6793
|
+
return undefined;
|
|
6794
|
+
}
|
|
6795
|
+
return value;
|
|
6796
|
+
};
|
|
6797
|
+
}
|
|
6798
|
+
function setterFn(name) {
|
|
6799
|
+
return function (value) {
|
|
6800
|
+
const stateEl = this.stateElement;
|
|
6801
|
+
if (!stateEl)
|
|
6802
|
+
return;
|
|
6803
|
+
stateEl.initializePromise.then(() => {
|
|
6804
|
+
stateEl.createState("writable", (state) => {
|
|
6805
|
+
state[name] = value;
|
|
6806
|
+
});
|
|
6807
|
+
});
|
|
6808
|
+
};
|
|
6809
|
+
}
|
|
6810
|
+
function callFn(name, isAsync) {
|
|
6811
|
+
if (isAsync) {
|
|
6812
|
+
return function (...args) {
|
|
6813
|
+
const stateEl = this.stateElement;
|
|
6814
|
+
if (!stateEl)
|
|
6815
|
+
return undefined;
|
|
6816
|
+
return stateEl.initializePromise.then(() => {
|
|
6817
|
+
let result;
|
|
6818
|
+
return stateEl.createStateAsync("writable", async (state) => {
|
|
6819
|
+
result = await state[name](...args);
|
|
6820
|
+
}).then(() => result);
|
|
6821
|
+
});
|
|
6822
|
+
};
|
|
6823
|
+
}
|
|
6824
|
+
return function (...args) {
|
|
6825
|
+
const stateEl = this.stateElement;
|
|
6826
|
+
if (!stateEl)
|
|
6827
|
+
return undefined;
|
|
6828
|
+
return stateEl.initializePromise.then(() => {
|
|
6829
|
+
let result;
|
|
6830
|
+
stateEl.createState("writable", (state) => {
|
|
6831
|
+
result = state[name](...args);
|
|
6832
|
+
});
|
|
6833
|
+
return result;
|
|
6834
|
+
});
|
|
6835
|
+
};
|
|
6836
|
+
}
|
|
6837
|
+
function isInternalProperty(name) {
|
|
6838
|
+
return name.startsWith("$");
|
|
6839
|
+
}
|
|
6840
|
+
|
|
6841
|
+
function createWcBindable(tagName, bindables) {
|
|
6842
|
+
const properties = bindables.map((propName) => ({
|
|
6843
|
+
name: propName,
|
|
6844
|
+
event: `${tagName}:${propName}-changed`,
|
|
6845
|
+
}));
|
|
6846
|
+
return {
|
|
6847
|
+
protocol: "wc-bindable",
|
|
6848
|
+
version: 1,
|
|
6849
|
+
properties,
|
|
6850
|
+
};
|
|
6851
|
+
}
|
|
6852
|
+
function createBindableEventMap(tagName, bindables) {
|
|
6853
|
+
const map = {};
|
|
6854
|
+
for (const propName of bindables) {
|
|
6855
|
+
map[propName] = `${tagName}:${propName}-changed`;
|
|
6856
|
+
}
|
|
6857
|
+
return map;
|
|
6858
|
+
}
|
|
6859
|
+
|
|
6860
|
+
function defineDCC(hostElement, shadowRoot, state) {
|
|
6861
|
+
const tagName = hostElement.tagName.toLowerCase();
|
|
6862
|
+
// バリデーション
|
|
6863
|
+
if (!tagName.includes("-")) {
|
|
6864
|
+
raiseError(`DCC: "${tagName}" is not a valid custom element name (must contain a hyphen).`);
|
|
6865
|
+
}
|
|
6866
|
+
if (customElements.get(tagName)) {
|
|
6867
|
+
// 既に登録済みならスキップ(重複定義の検知のため警告は出す)
|
|
6868
|
+
console.warn(`[@wcstack/state] DCC: "${tagName}" is already registered. Skipping redefinition.`);
|
|
5715
6869
|
return;
|
|
5716
6870
|
}
|
|
5717
6871
|
// ShadowRoot は cloneNode 不可のため、template 経由で内容をクローン
|
|
@@ -5949,7 +7103,8 @@ function dirtyCacheEntryByAbsoluteStateAddress(address) {
|
|
|
5949
7103
|
function checkDependency(handler, address) {
|
|
5950
7104
|
// 動的依存関係の登録
|
|
5951
7105
|
if (handler.addressStackLength > 0) {
|
|
5952
|
-
const
|
|
7106
|
+
const lastAddress = handler.lastAddressStack;
|
|
7107
|
+
const lastInfo = lastAddress?.pathInfo ?? null;
|
|
5953
7108
|
const stateElement = handler.stateElement;
|
|
5954
7109
|
if (lastInfo !== null) {
|
|
5955
7110
|
if (stateElement.getterPaths.has(lastInfo.path) &&
|
|
@@ -5957,6 +7112,27 @@ function checkDependency(handler, address) {
|
|
|
5957
7112
|
// lastInfo.pathはgetterの名前であり、address.pathInfo.pathは
|
|
5958
7113
|
// そのgetterが参照している値のパスである
|
|
5959
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
|
+
}
|
|
5960
7136
|
}
|
|
5961
7137
|
}
|
|
5962
7138
|
}
|
|
@@ -5981,19 +7157,42 @@ function checkDependency(handler, address) {
|
|
|
5981
7157
|
* - ワイルドカードや多重ループにも柔軟に対応し、再帰的な値取得を実現
|
|
5982
7158
|
* - finallyでキャッシュへの格納を保証
|
|
5983
7159
|
*/
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
7160
|
+
/**
|
|
7161
|
+
* namespace 配下のパスは raw state を持たないため、proxy の get トラップと同じ
|
|
7162
|
+
* namespace オブジェクトを辿る。1セグメント目は namespace 本体、2セグメント目以降は
|
|
7163
|
+
* namespace 上のキーを順に走査する。走査値が object / function 以外(null /
|
|
7164
|
+
* undefined / primitive の葉)になったら undefined を返す — 葉より深い読み
|
|
7165
|
+
* (例: `$streamStatus.<name>.<key>`、error が primitive throw のときの
|
|
7166
|
+
* `$streamError.<name>.message`)は宣言外アクセスと同じ undefined 解決とし、
|
|
7167
|
+
* Reflect.get の non-object TypeError を updater の drain に漏らさない
|
|
7168
|
+
* (§4-1 の throw しない寛容規約)。
|
|
7169
|
+
*/
|
|
7170
|
+
function walkNamespace(namespace, segments) {
|
|
7171
|
+
let value = namespace;
|
|
7172
|
+
for (let i = 1; i < segments.length; i++) {
|
|
7173
|
+
// Object(v) !== v は「v が object / function でない」(= primitive / null / undefined)判定
|
|
7174
|
+
if (Object(value) !== value) {
|
|
7175
|
+
return undefined;
|
|
5995
7176
|
}
|
|
5996
|
-
|
|
7177
|
+
value = Reflect.get(value, segments[i]);
|
|
7178
|
+
}
|
|
7179
|
+
return value;
|
|
7180
|
+
}
|
|
7181
|
+
function _getByAddress(target, address, receiver, handler, stateElement) {
|
|
7182
|
+
const firstSegment = address.pathInfo.segments[0];
|
|
7183
|
+
if (firstSegment === STATE_COMMAND_NAMESPACE_NAME) {
|
|
7184
|
+
// $command 名前空間: キーは宣言済み command token 名
|
|
7185
|
+
return walkNamespace(getCommandNamespace(stateElement), address.pathInfo.segments);
|
|
7186
|
+
}
|
|
7187
|
+
if (firstSegment === STATE_STREAM_STATUS_NAMESPACE_NAME) {
|
|
7188
|
+
// $streamStatus / $streamError 名前空間: キーは宣言済み stream 名
|
|
7189
|
+
// (registry entry が正本の thin gateway、docs/state-streams-design.md §4-2)。
|
|
7190
|
+
// setByAddress の親走査もここを通るため、子への Reflect.set が namespace proxy の
|
|
7191
|
+
// raiseError に到達する = 書き込み防御(S11)もこの分岐で成立する。
|
|
7192
|
+
return walkNamespace(getStreamStatusNamespace(stateElement), address.pathInfo.segments);
|
|
7193
|
+
}
|
|
7194
|
+
if (firstSegment === STATE_STREAM_ERROR_NAMESPACE_NAME) {
|
|
7195
|
+
return walkNamespace(getStreamErrorNamespace(stateElement), address.pathInfo.segments);
|
|
5997
7196
|
}
|
|
5998
7197
|
if (address.pathInfo.path in target) {
|
|
5999
7198
|
// getterの中で参照の可能性があるので、addressをプッシュする
|
|
@@ -6039,6 +7238,8 @@ function _getByAddressWithCache(target, address, receiver, handler, stateElement
|
|
|
6039
7238
|
}
|
|
6040
7239
|
function getByAddress(target, address, receiver, handler) {
|
|
6041
7240
|
checkDependency(handler, address);
|
|
7241
|
+
// $streams の args トレース中のみ絶対アドレスを捕捉(collector 非活性なら即 return)
|
|
7242
|
+
collectStreamDependency(handler.stateElement, address);
|
|
6042
7243
|
const stateElement = handler.stateElement;
|
|
6043
7244
|
const cacheable = address.pathInfo.wildcardCount > 0 ||
|
|
6044
7245
|
stateElement.getterPaths.has(address.pathInfo.path);
|
|
@@ -6082,49 +7283,6 @@ function getContextListIndex(handler, structuredPath) {
|
|
|
6082
7283
|
return address.listIndex?.at(index) ?? null;
|
|
6083
7284
|
}
|
|
6084
7285
|
|
|
6085
|
-
class Updater {
|
|
6086
|
-
_queueAbsoluteAddresses = [];
|
|
6087
|
-
constructor() {
|
|
6088
|
-
}
|
|
6089
|
-
enqueueAbsoluteAddress(absoluteAddress) {
|
|
6090
|
-
const requireStartProcess = this._queueAbsoluteAddresses.length === 0;
|
|
6091
|
-
this._queueAbsoluteAddresses.push(absoluteAddress);
|
|
6092
|
-
if (requireStartProcess) {
|
|
6093
|
-
queueMicrotask(() => {
|
|
6094
|
-
const absoluteAddresses = this._queueAbsoluteAddresses;
|
|
6095
|
-
this._queueAbsoluteAddresses = [];
|
|
6096
|
-
this._applyChange(absoluteAddresses);
|
|
6097
|
-
});
|
|
6098
|
-
}
|
|
6099
|
-
}
|
|
6100
|
-
// テスト用に公開
|
|
6101
|
-
testApplyChange(absoluteAddresses) {
|
|
6102
|
-
this._applyChange(absoluteAddresses);
|
|
6103
|
-
}
|
|
6104
|
-
_applyChange(absoluteAddresses) {
|
|
6105
|
-
// Note: AbsoluteStateAddress はキャッシュされているため、
|
|
6106
|
-
// 同一の (stateName, address) は同じインスタンスとなり、
|
|
6107
|
-
// Set による重複排除が正しく機能する
|
|
6108
|
-
const absoluteAddressSet = new Set(absoluteAddresses);
|
|
6109
|
-
const processBindings = [];
|
|
6110
|
-
for (const absoluteAddress of absoluteAddressSet) {
|
|
6111
|
-
const bindings = getBindingSetByAbsoluteStateAddress(absoluteAddress);
|
|
6112
|
-
for (const binding of bindings) {
|
|
6113
|
-
if (binding.replaceNode.isConnected === false) {
|
|
6114
|
-
// 切断されているバインディングは無視
|
|
6115
|
-
continue;
|
|
6116
|
-
}
|
|
6117
|
-
processBindings.push(binding);
|
|
6118
|
-
}
|
|
6119
|
-
}
|
|
6120
|
-
applyChangeFromBindings(processBindings);
|
|
6121
|
-
}
|
|
6122
|
-
}
|
|
6123
|
-
const updater = new Updater();
|
|
6124
|
-
function getUpdater() {
|
|
6125
|
-
return updater;
|
|
6126
|
-
}
|
|
6127
|
-
|
|
6128
7286
|
const swapInfoByStateAddress = new WeakMap();
|
|
6129
7287
|
function getSwapInfoByAddress(address) {
|
|
6130
7288
|
return swapInfoByStateAddress.get(address) ?? null;
|
|
@@ -6177,6 +7335,37 @@ function _walkExpandWildcard(context, currentWildcardIndex, parentListIndex) {
|
|
|
6177
7335
|
}
|
|
6178
7336
|
}
|
|
6179
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
|
+
}
|
|
6180
7369
|
function _walkDependency(context, startAddress, callback) {
|
|
6181
7370
|
const stack = [{ address: startAddress, depth: 0 }];
|
|
6182
7371
|
while (stack.length > 0) {
|
|
@@ -6209,7 +7398,7 @@ function _walkDependency(context, startAddress, callback) {
|
|
|
6209
7398
|
const absAddress = createAbsoluteStateAddress(absPathInfo, address.listIndex);
|
|
6210
7399
|
const lastValue = getLastListValueByAbsoluteStateAddress(absAddress);
|
|
6211
7400
|
const listDiff = createListDiff(address.listIndex, lastValue, newValue);
|
|
6212
|
-
for (const listIndex of listDiff
|
|
7401
|
+
for (const listIndex of selectExpansionIndexes(context, sourcePath, lastValue, newValue, listDiff)) {
|
|
6213
7402
|
const depAddress = createStateAddress(depPathInfo, listIndex);
|
|
6214
7403
|
context.result.add(depAddress);
|
|
6215
7404
|
nextEntries.push({ address: depAddress, depth: nextDepth });
|
|
@@ -6303,7 +7492,7 @@ function _walkDependency(context, startAddress, callback) {
|
|
|
6303
7492
|
}
|
|
6304
7493
|
}
|
|
6305
7494
|
}
|
|
6306
|
-
function walkDependency(stateName, stateElement, startAddress, staticDependency, dynamicDependency, listPathSet, stateProxy, searchType, callback) {
|
|
7495
|
+
function walkDependency(stateName, stateElement, startAddress, staticDependency, dynamicDependency, listPathSet, stateProxy, searchType, callback, options) {
|
|
6307
7496
|
const context = {
|
|
6308
7497
|
stateElement: stateElement,
|
|
6309
7498
|
staticMap: staticDependency,
|
|
@@ -6313,6 +7502,7 @@ function walkDependency(stateName, stateElement, startAddress, staticDependency,
|
|
|
6313
7502
|
visited: new Set(),
|
|
6314
7503
|
stateProxy: stateProxy,
|
|
6315
7504
|
searchType: searchType,
|
|
7505
|
+
listExpansion: options?.listExpansion ?? "full",
|
|
6316
7506
|
};
|
|
6317
7507
|
_walkDependency(context, startAddress, callback);
|
|
6318
7508
|
return Array.from(context.result);
|
|
@@ -6378,7 +7568,10 @@ function _setByAddress(target, address, absAddress, value, receiver, handler) {
|
|
|
6378
7568
|
dirtyCacheEntryByAbsoluteStateAddress(absDepAddress);
|
|
6379
7569
|
// 更新対象として登録
|
|
6380
7570
|
updater.enqueueAbsoluteAddress(absDepAddress);
|
|
6381
|
-
}
|
|
7571
|
+
},
|
|
7572
|
+
// リスト置換時は追加行・位置変更行のみ展開する(未変更行の再訪を省く。
|
|
7573
|
+
// $postUpdate の手動リフレッシュは従来通り全行展開のまま)
|
|
7574
|
+
{ listExpansion: "diff" });
|
|
6382
7575
|
}
|
|
6383
7576
|
}
|
|
6384
7577
|
function _setByAddressWithSwap(target, address, absAddress, value, receiver, handler) {
|
|
@@ -6821,6 +8014,9 @@ async function setLoopContextAsync(handler, loopContext, callback) {
|
|
|
6821
8014
|
* - 通常のプロパティアクセスもバインディングや多重ループに対応
|
|
6822
8015
|
* - シンボルAPIやReflect.getで拡張性・互換性も確保
|
|
6823
8016
|
*/
|
|
8017
|
+
// `$streamStatus.<name>` / `$streamError.<name>` の dotted パス判定用プレフィックス
|
|
8018
|
+
const STREAM_STATUS_PATH_PREFIX = `${STATE_STREAM_STATUS_NAMESPACE_NAME}${DELIMITER}`;
|
|
8019
|
+
const STREAM_ERROR_PATH_PREFIX = `${STATE_STREAM_ERROR_NAMESPACE_NAME}${DELIMITER}`;
|
|
6824
8020
|
function get(target, prop, receiver, handler) {
|
|
6825
8021
|
const index = INDEX_BY_INDEX_NAME[prop];
|
|
6826
8022
|
if (typeof index !== "undefined") {
|
|
@@ -6859,14 +8055,27 @@ function get(target, prop, receiver, handler) {
|
|
|
6859
8055
|
case STATE_COMMAND_NAMESPACE_NAME: {
|
|
6860
8056
|
return getCommandNamespace(handler.stateElement);
|
|
6861
8057
|
}
|
|
8058
|
+
case STATE_STREAM_STATUS_NAMESPACE_NAME: {
|
|
8059
|
+
return getStreamStatusNamespace(handler.stateElement);
|
|
8060
|
+
}
|
|
8061
|
+
case STATE_STREAM_ERROR_NAMESPACE_NAME: {
|
|
8062
|
+
return getStreamErrorNamespace(handler.stateElement);
|
|
8063
|
+
}
|
|
8064
|
+
}
|
|
8065
|
+
// switch 不一致の $ プロパティのうち、`$streamStatus.<name>` / `$streamError.<name>`
|
|
8066
|
+
// の dotted パスだけは通常のパス解決(getByAddress)へフォールスルーさせる。
|
|
8067
|
+
// これが computed(getter)内での依存追跡付き読み取りの正規形
|
|
8068
|
+
// (checkDependency が getter スコープで動的依存を登録し、$postUpdate の
|
|
8069
|
+
// walkDependency で computed が無効化される、docs/state-streams-design.md §4-3)。
|
|
8070
|
+
// それ以外の未知 $ プロパティは従来どおり undefined を返す。
|
|
8071
|
+
if (!prop.startsWith(STREAM_STATUS_PATH_PREFIX) && !prop.startsWith(STREAM_ERROR_PATH_PREFIX)) {
|
|
8072
|
+
return undefined;
|
|
6862
8073
|
}
|
|
6863
8074
|
}
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
return getByAddress(target, stateAddress, receiver, handler);
|
|
6869
|
-
}
|
|
8075
|
+
const resolvedAddress = getResolvedAddress(prop);
|
|
8076
|
+
const listIndex = getListIndex(target, resolvedAddress, receiver, handler);
|
|
8077
|
+
const stateAddress = createStateAddress(resolvedAddress.pathInfo, listIndex);
|
|
8078
|
+
return getByAddress(target, stateAddress, receiver, handler);
|
|
6870
8079
|
}
|
|
6871
8080
|
else if (typeof prop === "symbol") {
|
|
6872
8081
|
switch (prop) {
|
|
@@ -7441,6 +8650,10 @@ class State extends HTMLElement {
|
|
|
7441
8650
|
return getBindingsReady(rootNode);
|
|
7442
8651
|
}
|
|
7443
8652
|
__state;
|
|
8653
|
+
_hasUpdatedCallback = false;
|
|
8654
|
+
// 他行を読む getter が検出されたリストパス(diff-filter 展開の全行フォールバック対象)。
|
|
8655
|
+
// 依存マップ(static/dynamic)と同様に追加のみ・クリアしない(安全側に固定される)。
|
|
8656
|
+
_crossRowListPaths = new Set();
|
|
7444
8657
|
_name = 'default';
|
|
7445
8658
|
_initialized = false;
|
|
7446
8659
|
_initializePromise;
|
|
@@ -7466,6 +8679,17 @@ class State extends HTMLElement {
|
|
|
7466
8679
|
_bindableEventMap = {};
|
|
7467
8680
|
_commandTokenNames = new Set();
|
|
7468
8681
|
_eventTokenNames = new Set();
|
|
8682
|
+
_dcc = false;
|
|
8683
|
+
// connect サイクルの世代カウンタ(connectedCallback 冒頭でインクリメント)。
|
|
8684
|
+
// $connectedCallback の await 中の「切断 → 即再接続」では、新 connect が
|
|
8685
|
+
// _rootNode を再設定済みのため陳腐化した旧 connect の再開が _rootNode ガードを
|
|
8686
|
+
// 素通りして startStreams に到達し、同一の再接続に対して source が二重起動する。
|
|
8687
|
+
// 末尾で冒頭に捕捉した世代と照合し、陳腐 connect からの起動を skip する(設計書 §2-3)。
|
|
8688
|
+
_connectGeneration = 0;
|
|
8689
|
+
// _state セッター側の startStreams が走った connect 世代
|
|
8690
|
+
// (connectedCallback 末尾の startStreams との二重起動防止、設計書 §2-3。
|
|
8691
|
+
// 世代が進めば不一致となり自然に無効化される — サイクル単位のフラグリセット相当)
|
|
8692
|
+
_streamsStartedGeneration = 0;
|
|
7469
8693
|
constructor() {
|
|
7470
8694
|
super();
|
|
7471
8695
|
this._initializePromise = new Promise((resolve) => {
|
|
@@ -7491,12 +8715,22 @@ class State extends HTMLElement {
|
|
|
7491
8715
|
this._commandTokenNames = processCommandTokensDeclaration(value);
|
|
7492
8716
|
this._eventTokenNames = processEventTokensDeclaration(value);
|
|
7493
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;
|
|
7494
8725
|
// 再 set 時に二重 subscribe しないよう registry をクリアしてから $on を配線し直す。
|
|
7495
8726
|
clearEventTokenRegistry(this);
|
|
7496
8727
|
processOnDeclaration(this, value, this._eventTokenNames);
|
|
7497
8728
|
this._listPaths.clear();
|
|
7498
8729
|
this._elementPaths.clear();
|
|
7499
8730
|
this._getterPaths.clear();
|
|
8731
|
+
// 再 set 時の残骸が $streams の衝突検査(processStreamsDeclaration)に
|
|
8732
|
+
// 偽陽性で命中しないよう getterPaths と対称にクリアする。
|
|
8733
|
+
this._setterPaths.clear();
|
|
7500
8734
|
this._pathSet.clear();
|
|
7501
8735
|
const stateInfo = getStateInfo(value);
|
|
7502
8736
|
for (const path of stateInfo.getterPaths) {
|
|
@@ -7505,6 +8739,25 @@ class State extends HTMLElement {
|
|
|
7505
8739
|
for (const path of stateInfo.setterPaths) {
|
|
7506
8740
|
this._setterPaths.add(path);
|
|
7507
8741
|
}
|
|
8742
|
+
// $streams: 再 set 時の二重起動防止のため旧 stream を abort + registry 全削除してから
|
|
8743
|
+
// 新宣言をパースする(clearEventTokenRegistry → processOnDeclaration と同じ再配線パターン)。
|
|
8744
|
+
// getterPaths / setterPaths の収集後であること(宣言バリデーションが衝突検査で参照する)。
|
|
8745
|
+
// namespace proxy の memo も破棄して古い proxy を捨てる(clearCommandNamespace と対称)。
|
|
8746
|
+
clearStreamNamespace(this);
|
|
8747
|
+
clearStreamRegistry(this);
|
|
8748
|
+
processStreamsDeclaration(this, value);
|
|
8749
|
+
// 接続中の再 set(S13)は新宣言で即再起動する。
|
|
8750
|
+
// 初回(_initialize 中)は _initialized が false なのでここでは起動されず、
|
|
8751
|
+
// connectedCallback 側の startStreams($connectedCallback 完了後)が担う。
|
|
8752
|
+
if (this._initialized && this._rootNode !== null && !inSsr()) {
|
|
8753
|
+
startStreams(this);
|
|
8754
|
+
// $connectedCallback 実行中の再 set(setInitialState)では、ここで新宣言が
|
|
8755
|
+
// 起動済みのため connectedCallback 末尾の startStreams を skip させる。
|
|
8756
|
+
// skip しないと同一 connect サイクルで新宣言の source が 2 回起動する
|
|
8757
|
+
// (1 回目は即 abort — switchMap 意味論で状態は壊れないが、副作用を持つ
|
|
8758
|
+
// source が 2 回発火してしまう)。
|
|
8759
|
+
this._streamsStartedGeneration = this._connectGeneration;
|
|
8760
|
+
}
|
|
7508
8761
|
this._resolveLoading?.();
|
|
7509
8762
|
}
|
|
7510
8763
|
get name() {
|
|
@@ -7655,6 +8908,7 @@ class State extends HTMLElement {
|
|
|
7655
8908
|
raiseError(`DCC: Failed to load state: ${e}`);
|
|
7656
8909
|
}
|
|
7657
8910
|
defineDCC(hostElement, shadowRoot, state);
|
|
8911
|
+
this._dcc = true;
|
|
7658
8912
|
this._initialized = true;
|
|
7659
8913
|
this._rootNode = null; // disconnectedCallbackでのstate参照を防止
|
|
7660
8914
|
this._resolveInitialize?.();
|
|
@@ -7670,6 +8924,11 @@ class State extends HTMLElement {
|
|
|
7670
8924
|
}
|
|
7671
8925
|
async connectedCallback() {
|
|
7672
8926
|
this._rootNode = this.getRootNode();
|
|
8927
|
+
// connect 世代を進めて冒頭で捕捉する(末尾の startStreams 前に照合し、
|
|
8928
|
+
// $connectedCallback の await 中に「切断 → 即再接続」された陳腐 connect の
|
|
8929
|
+
// 再開からの起動を防ぐ)。前回接続中の再 set(S13)で立った
|
|
8930
|
+
// _streamsStartedGeneration も世代不一致となり自然に無効化される。
|
|
8931
|
+
const connectGeneration = ++this._connectGeneration;
|
|
7673
8932
|
if (!this._initialized) {
|
|
7674
8933
|
// DCC 検出: ShadowRoot 内かつホストに data-wc-definition がある場合
|
|
7675
8934
|
const parentNode = this.parentNode;
|
|
@@ -7683,6 +8942,12 @@ class State extends HTMLElement {
|
|
|
7683
8942
|
this._initialized = true;
|
|
7684
8943
|
this._resolveInitialize?.();
|
|
7685
8944
|
}
|
|
8945
|
+
else if (!this._dcc && getStateElementByName(this._rootNode, this._name) !== this) {
|
|
8946
|
+
// 再接続(disconnect で名前登録が解除された後の再 connect): 登録を復元する。
|
|
8947
|
+
// createState が rootNode 経由でこの要素を解決できるようにするために必要
|
|
8948
|
+
// ($connectedCallback の再実行と $streams の initial からの再起動が依存する、設計書 §2-3)。
|
|
8949
|
+
setStateElementByName(this._rootNode, this._name, this);
|
|
8950
|
+
}
|
|
7686
8951
|
// enable-ssr (クライアント側): SSR で $connectedCallback 済みなのでスキップ
|
|
7687
8952
|
// inSsr() (サーバー側): レンダリング中なので実行する
|
|
7688
8953
|
if (!this.hasAttribute('enable-ssr') || inSsr()) {
|
|
@@ -7699,16 +8964,53 @@ class State extends HTMLElement {
|
|
|
7699
8964
|
Ssr.buildContent(ssrEl, stateData);
|
|
7700
8965
|
this.parentNode?.insertBefore(ssrEl, this);
|
|
7701
8966
|
}
|
|
8967
|
+
// $streams の eager 起動($connectedCallback 完了後、設計書 §2-3)。
|
|
8968
|
+
// inSsr() 時は起動しない(SSR 出力には initial が乗る、§7-1)。
|
|
8969
|
+
// enable-ssr のクライアント側は $connectedCallback をスキップしても起動する
|
|
8970
|
+
// (stream はシリアライズ不能なランタイム副作用のため)。
|
|
8971
|
+
// _rootNode ガード: $connectedCallback の await 中に切断された場合は起動しない。
|
|
8972
|
+
// ガードなしだと startStream 内の createState が rootNode 解決(disconnectedCallback
|
|
8973
|
+
// で null 化済み)の raiseError で throw し、connectedCallbackPromise が永遠に
|
|
8974
|
+
// 未解決になる。「未接続の entry は restart しない」設計書 §3-2 とも整合し、
|
|
8975
|
+
// _state セッター側の startStreams 前ガード(_rootNode !== null)と対称。
|
|
8976
|
+
// 世代ガード(connectGeneration 照合): await 中に「切断 → 即再接続」された場合、
|
|
8977
|
+
// 新 connect が _rootNode を再設定済みで上のガードを素通りするため、世代不一致で
|
|
8978
|
+
// 陳腐化した connect の再開を検出して skip する。起動点が新 connect の末尾に
|
|
8979
|
+
// 一本化され、「$connectedCallback 完了後に起動」(S1)の順序保証も保たれる。
|
|
8980
|
+
// _streamsStartedGeneration ガード: $connectedCallback 内の setInitialState
|
|
8981
|
+
// (接続中の再 set)で _state セッター側が新宣言を起動済みの場合は skip する
|
|
8982
|
+
// (skip しないと同一 connect サイクルで source が 2 回起動する、設計書 §2-3)。
|
|
8983
|
+
if (!inSsr() &&
|
|
8984
|
+
this._rootNode !== null &&
|
|
8985
|
+
connectGeneration === this._connectGeneration &&
|
|
8986
|
+
this._streamsStartedGeneration !== connectGeneration) {
|
|
8987
|
+
startStreams(this);
|
|
8988
|
+
}
|
|
7702
8989
|
this._resolveConnectedCallback?.();
|
|
7703
8990
|
}
|
|
7704
8991
|
disconnectedCallback() {
|
|
7705
8992
|
if (this._rootNode !== null) {
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
8993
|
+
// try/finally: ユーザーの $disconnectedCallback が throw しても後続の後始末を
|
|
8994
|
+
// 必ず実行する。特に abortAllStreams が飛ぶと stream が消費を続け(ゾンビ I/O)、
|
|
8995
|
+
// activeStateElements の強参照残留で GC が妨げられ、切断済み要素が依存駆動
|
|
8996
|
+
// restart の対象にも残る(設計書 §3-2 / §5-1 違反)。throw 自体は従来どおり
|
|
8997
|
+
// 呼び出し元へ伝播させる(変わるのは後始末の保証のみ)。
|
|
8998
|
+
try {
|
|
8999
|
+
this._callStateDisconnectedCallback();
|
|
9000
|
+
}
|
|
9001
|
+
finally {
|
|
9002
|
+
setStateElementByName(this.rootNode, this._name, null);
|
|
9003
|
+
clearCommandTokenRegistry(this);
|
|
9004
|
+
clearCommandNamespace(this);
|
|
9005
|
+
clearEventTokenRegistry(this);
|
|
9006
|
+
// stream は abort のみで registry は保持する(再接続時に同じ宣言から
|
|
9007
|
+
// initial で再起動できる、設計書 §5-1 / §5-2)。
|
|
9008
|
+
// namespace proxy の memo は破棄する(clearCommandNamespace と対称。
|
|
9009
|
+
// registry は残るため再接続後の初回アクセスで同内容の proxy が再生成される)。
|
|
9010
|
+
abortAllStreams(this);
|
|
9011
|
+
clearStreamNamespace(this);
|
|
9012
|
+
this._rootNode = null;
|
|
9013
|
+
}
|
|
7712
9014
|
}
|
|
7713
9015
|
}
|
|
7714
9016
|
get initializePromise() {
|
|
@@ -7842,8 +9144,20 @@ class State extends HTMLElement {
|
|
|
7842
9144
|
this._version++;
|
|
7843
9145
|
return this._version;
|
|
7844
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
|
+
}
|
|
7845
9156
|
bindProperty(prop, desc) {
|
|
7846
9157
|
Object.defineProperty(this._state, prop, desc);
|
|
9158
|
+
if (prop === STATE_UPDATED_CALLBACK_NAME) {
|
|
9159
|
+
this._hasUpdatedCallback = true;
|
|
9160
|
+
}
|
|
7847
9161
|
}
|
|
7848
9162
|
setInitialState(state) {
|
|
7849
9163
|
if (!this._initialized) {
|