@wcstack/state 1.23.0 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +0 -2
- package/dist/index.esm.js +417 -159
- 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
|
@@ -1918,7 +1918,6 @@ function setLastListValueByAbsoluteStateAddress(address, value) {
|
|
|
1918
1918
|
lastListValueByAbsoluteStateAddress.set(address, value);
|
|
1919
1919
|
}
|
|
1920
1920
|
|
|
1921
|
-
const setLoopContextAsyncSymbol = Symbol("$$setLoopContextAsync");
|
|
1922
1921
|
const setLoopContextSymbol = Symbol("$$setLoopContext");
|
|
1923
1922
|
const getByAddressSymbol = Symbol("$$getByAddress");
|
|
1924
1923
|
const hasByAddressSymbol = Symbol("$$hasByAddress");
|
|
@@ -2489,6 +2488,54 @@ function detachCheckboxEventHandler(binding) {
|
|
|
2489
2488
|
return false;
|
|
2490
2489
|
}
|
|
2491
2490
|
|
|
2491
|
+
/**
|
|
2492
|
+
* captureHandlerRejection.ts
|
|
2493
|
+
*
|
|
2494
|
+
* state 側ハンドラ(`$on` の event-token subscriber、`onXxx:` の state メソッド、
|
|
2495
|
+
* DOM イベント起点の command-token emit)の戻り値を受け取り、Promise が混ざって
|
|
2496
|
+
* いれば reject を捕捉して報告する。
|
|
2497
|
+
*
|
|
2498
|
+
* なぜ必要か:
|
|
2499
|
+
* 発火経路はハンドラの完了を待たない。戻り値は `Token.emit` の結果配列にしか現れず、
|
|
2500
|
+
* 呼び出し側(eventTokenHandler / handler)はそれを捨てている。そのため **async
|
|
2501
|
+
* ハンドラが reject すると unhandled rejection になり**、しかも「どのハンドラで
|
|
2502
|
+
* 落ちたか」の手掛かりがスタックにしか残らない(特性化:
|
|
2503
|
+
* `__tests__/poc.asyncOnLoopContext.test.ts`)。
|
|
2504
|
+
*
|
|
2505
|
+
* 握り潰しではない:
|
|
2506
|
+
* 可視性は `console.error` で保たれ、state 名・ハンドラ名が付く分むしろ特定は容易に
|
|
2507
|
+
* なる。非同期の失敗を「例外の伝播」ではなく「診断可能な報告」に落とすのは
|
|
2508
|
+
* never-throw(async-io-node-guidelines.md §3.6)と同じ方針であり、I/O ノードが
|
|
2509
|
+
* `error` プロパティへ流すのと同じ位置づけの、state 側ハンドラ版にあたる。
|
|
2510
|
+
*
|
|
2511
|
+
* 同期 throw はここを通らない(従来どおり呼び出し元へ伝播する)。プログラマエラーを
|
|
2512
|
+
* loud に落とす `raiseError` の挙動は一切変えない。
|
|
2513
|
+
*/
|
|
2514
|
+
function isThenable(value) {
|
|
2515
|
+
return ((typeof value === "object" || typeof value === "function") &&
|
|
2516
|
+
value !== null &&
|
|
2517
|
+
typeof value.then === "function");
|
|
2518
|
+
}
|
|
2519
|
+
/**
|
|
2520
|
+
* @param result ハンドラ呼び出しの戻り値。`Token.emit` の結果配列(subscriber ごとの
|
|
2521
|
+
* 戻り値)と、単一ハンドラの戻り値の両方を受ける。
|
|
2522
|
+
* @param describe 報告に載せるハンドラの識別名(例: `$on."rowFailed" of state "default"`)。
|
|
2523
|
+
*/
|
|
2524
|
+
function captureHandlerRejection(result, describe) {
|
|
2525
|
+
// emit は subscriber ごとの戻り値配列。単一ハンドラの戻り値はそのまま届く。
|
|
2526
|
+
const values = Array.isArray(result) ? result : [result];
|
|
2527
|
+
for (const value of values) {
|
|
2528
|
+
if (!isThenable(value)) {
|
|
2529
|
+
continue;
|
|
2530
|
+
}
|
|
2531
|
+
// Promise.resolve は native Promise をそのまま返すため、catch の登録によって
|
|
2532
|
+
// 元の Promise が handled になる(thenable は同値の Promise に包まれる)。
|
|
2533
|
+
Promise.resolve(value).catch((error) => {
|
|
2534
|
+
console.error(`[wcstack/state] ${describe} rejected.`, error);
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2492
2539
|
// command-token / event-token が共有する pub/sub プリミティブ。
|
|
2493
2540
|
// _subscribers は Set のため挿入順を保持する。
|
|
2494
2541
|
// emit() は subscribe() された順に呼び出され、戻り値配列も同じ順序で返る。
|
|
@@ -2654,11 +2701,14 @@ function attachEventTokenHandler(binding) {
|
|
|
2654
2701
|
}
|
|
2655
2702
|
const loopContext = getLoopContextByNode(element);
|
|
2656
2703
|
stateElement.createStateAsync("writable", async (state) => {
|
|
2657
|
-
state[setLoopContextSymbol](loopContext, () => {
|
|
2704
|
+
const results = state[setLoopContextSymbol](loopContext, () => {
|
|
2658
2705
|
const indexes = loopContext?.listIndex.indexes ?? [];
|
|
2659
2706
|
const token = getOrCreateEventToken(stateElement, tokenName);
|
|
2660
2707
|
return token.emit(state, event, ...indexes);
|
|
2661
2708
|
});
|
|
2709
|
+
// この経路はハンドラの完了を待たない(emit の戻り値はここでしか見えない)。
|
|
2710
|
+
// async な $on ハンドラの reject を unhandled にせず報告へ落とす。
|
|
2711
|
+
captureHandlerRejection(results, `$on."${tokenName}" of state "${stateName}"`);
|
|
2662
2712
|
});
|
|
2663
2713
|
};
|
|
2664
2714
|
element.addEventListener(eventName, handler);
|
|
@@ -2736,7 +2786,7 @@ const stateEventHandlerFunction = (stateName, handlerName, modifiers, statePathI
|
|
|
2736
2786
|
const loopContext = getLoopContextByNode(node);
|
|
2737
2787
|
const isCommand = isCommandTokenPath(handlerName);
|
|
2738
2788
|
stateElement.createStateAsync("writable", async (state) => {
|
|
2739
|
-
state[setLoopContextSymbol](loopContext, () => {
|
|
2789
|
+
const results = state[setLoopContextSymbol](loopContext, () => {
|
|
2740
2790
|
const indexes = loopContext?.listIndex.indexes ?? [];
|
|
2741
2791
|
if (isCommand) {
|
|
2742
2792
|
// command token を解決して emit。引数はハンドラ呼び出しと同じく (event, ...listIndexes) を透過する。
|
|
@@ -2752,6 +2802,9 @@ const stateEventHandlerFunction = (stateName, handlerName, modifiers, statePathI
|
|
|
2752
2802
|
}
|
|
2753
2803
|
return Reflect.apply(handler, state, [event, ...indexes]);
|
|
2754
2804
|
});
|
|
2805
|
+
// eventTokenHandler と同じく、この経路もハンドラの完了を待たない。async な
|
|
2806
|
+
// state メソッド / command subscriber の reject を unhandled にせず報告へ落とす。
|
|
2807
|
+
captureHandlerRejection(results, `"${handlerName}" of state "${stateName}"`);
|
|
2755
2808
|
});
|
|
2756
2809
|
};
|
|
2757
2810
|
function attachEventHandler(binding) {
|
|
@@ -3026,14 +3079,53 @@ function matchWriteReceipt(node, member) {
|
|
|
3026
3079
|
return null;
|
|
3027
3080
|
}
|
|
3028
3081
|
|
|
3082
|
+
/**
|
|
3083
|
+
* occurrenceWrite.ts
|
|
3084
|
+
*
|
|
3085
|
+
* wc-bindable の `semantics: "event"` を宣言した property から届いた値を state へ書き込む
|
|
3086
|
+
* 間だけ、same-value guard(`config.sameValueGuard`・既定 ON)を 1 回分だけ無効化する
|
|
3087
|
+
* one-shot トークン。
|
|
3088
|
+
*
|
|
3089
|
+
* 背景:
|
|
3090
|
+
* same-value guard は primitive が `Object.is` 同値なら set / enqueue / 依存伝播 / DOM 適用 /
|
|
3091
|
+
* `$updatedCallback` をまるごとスキップする。current value(state)にとっては正しい最適化だが、
|
|
3092
|
+
* occurrence(同じ payload でも「もう一度起きた」ことに意味がある)へ適用すると発生を取りこぼす。
|
|
3093
|
+
* どちらであるかは producer の declaration が `semantics` で宣言する
|
|
3094
|
+
* (docs/architecture-hardening/12-wc-bindable-observable-inventory.md)。
|
|
3095
|
+
*
|
|
3096
|
+
* one-shot にしている理由:
|
|
3097
|
+
* フラグを書き込みの呼び出しスタック全体へ張ると、その内側で走る `$updatedCallback` や
|
|
3098
|
+
* 依存伝播が行う無関係な書き込みまでガードを失う。`setByAddress` が最初のガード評価で
|
|
3099
|
+
* トークンを消費するため、影響は目的の 1 write に閉じる。
|
|
3100
|
+
*/
|
|
3101
|
+
let pending = false;
|
|
3102
|
+
/** 直後の 1 write を occurrence として扱う。必ず `endOccurrenceWrite` と対で使う。 */
|
|
3103
|
+
function beginOccurrenceWrite() {
|
|
3104
|
+
pending = true;
|
|
3105
|
+
}
|
|
3106
|
+
/** 未消費のトークンを破棄する(write が setByAddress へ到達しなかった場合の後始末)。 */
|
|
3107
|
+
function endOccurrenceWrite() {
|
|
3108
|
+
pending = false;
|
|
3109
|
+
}
|
|
3110
|
+
/**
|
|
3111
|
+
* ガード評価側が呼ぶ。`true` を返したら、その 1 回だけ same-value guard を飛ばす。
|
|
3112
|
+
* トークンは呼んだ時点で消費される。
|
|
3113
|
+
*/
|
|
3114
|
+
function consumeOccurrenceWrite() {
|
|
3115
|
+
if (!pending)
|
|
3116
|
+
return false;
|
|
3117
|
+
pending = false;
|
|
3118
|
+
return true;
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3029
3121
|
const handlerByHandlerKey = new Map();
|
|
3030
3122
|
// binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
|
|
3031
3123
|
const bindingRegistry = createHandlerBindingRegistry();
|
|
3032
3124
|
const producerValueObserversByNode = new WeakMap();
|
|
3033
3125
|
const DEFAULT_GETTER = (e) => e.detail;
|
|
3034
|
-
function getHandlerKey(binding, eventName, hasGetter) {
|
|
3126
|
+
function getHandlerKey(binding, eventName, hasGetter, isOccurrence) {
|
|
3035
3127
|
const filterKey = binding.inFilters.map(f => f.filterName + '(' + f.args.join(',') + ')').join('|');
|
|
3036
|
-
return `${binding.stateName}::${binding.propName}::${binding.statePathName}::${eventName}::${filterKey}::${hasGetter ? 'g' : 'n'}`;
|
|
3128
|
+
return `${binding.stateName}::${binding.propName}::${binding.statePathName}::${eventName}::${filterKey}::${hasGetter ? 'g' : 'n'}::${isOccurrence ? 'o' : 's'}`;
|
|
3037
3129
|
}
|
|
3038
3130
|
function getEventName(binding) {
|
|
3039
3131
|
const tagName = binding.node.tagName.toLowerCase();
|
|
@@ -3069,7 +3161,20 @@ function getValueGetter(binding) {
|
|
|
3069
3161
|
}
|
|
3070
3162
|
return null;
|
|
3071
3163
|
}
|
|
3072
|
-
|
|
3164
|
+
/**
|
|
3165
|
+
* producer が `semantics: "event"` を宣言した property か。occurrence は同じ payload でも
|
|
3166
|
+
* 「もう一度起きた」ことに意味があるため、state への書き込みで same-value guard を通さない
|
|
3167
|
+
* (docs/async-io-node-guidelines.md §3.3.1 の `event`)。宣言が無い property は従来どおり
|
|
3168
|
+
* — 未指定は「未指定」であって state ではないので、挙動は変えない。
|
|
3169
|
+
*/
|
|
3170
|
+
function isOccurrenceProperty(binding) {
|
|
3171
|
+
const customTagName = getCustomElement(binding.node);
|
|
3172
|
+
if (customTagName === null)
|
|
3173
|
+
return false;
|
|
3174
|
+
const propDesc = readBindableDeclaration(binding.node)?.knownProperties.get(binding.propName);
|
|
3175
|
+
return propDesc?.semantics === "event";
|
|
3176
|
+
}
|
|
3177
|
+
const twowayEventHandlerFunction = (stateName, propName, statePathName, inFilters, valueGetter, isOccurrence) => (event) => {
|
|
3073
3178
|
const node = event.target;
|
|
3074
3179
|
if (node === null) {
|
|
3075
3180
|
console.warn(`[@wcstack/state] event.target is null.`);
|
|
@@ -3155,11 +3260,21 @@ const twowayEventHandlerFunction = (stateName, propName, statePathName, inFilter
|
|
|
3155
3260
|
}
|
|
3156
3261
|
const loopContext = getLoopContextByNode(node);
|
|
3157
3262
|
const commitToState = () => {
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3263
|
+
// occurrence は同値でも取りこぼしてはならない(§3.3.1 `event`)。トークンは
|
|
3264
|
+
// setByAddress の最初のガード評価で消費されるため、この write 1 回だけに効く。
|
|
3265
|
+
if (isOccurrence)
|
|
3266
|
+
beginOccurrenceWrite();
|
|
3267
|
+
try {
|
|
3268
|
+
stateElement.createState("writable", (state) => {
|
|
3269
|
+
state[setLoopContextSymbol](loopContext, () => {
|
|
3270
|
+
state[statePathName] = filteredNewValue;
|
|
3271
|
+
});
|
|
3161
3272
|
});
|
|
3162
|
-
}
|
|
3273
|
+
}
|
|
3274
|
+
finally {
|
|
3275
|
+
if (isOccurrence)
|
|
3276
|
+
endOccurrenceWrite();
|
|
3277
|
+
}
|
|
3163
3278
|
};
|
|
3164
3279
|
if (propagationContext !== null) {
|
|
3165
3280
|
runWithPropagationContext(propagationContext, commitToState);
|
|
@@ -3203,10 +3318,11 @@ function attachTwowayEventHandler(binding) {
|
|
|
3203
3318
|
if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf('ro') === -1) {
|
|
3204
3319
|
const eventName = getEventName(binding);
|
|
3205
3320
|
const valueGetter = getValueGetter(binding);
|
|
3206
|
-
const
|
|
3321
|
+
const isOccurrence = isOccurrenceProperty(binding);
|
|
3322
|
+
const key = getHandlerKey(binding, eventName, valueGetter !== null, isOccurrence);
|
|
3207
3323
|
let twowayEventHandler = handlerByHandlerKey.get(key);
|
|
3208
3324
|
if (typeof twowayEventHandler === "undefined") {
|
|
3209
|
-
twowayEventHandler = twowayEventHandlerFunction(binding.stateName, binding.propName, binding.statePathName, binding.inFilters, valueGetter);
|
|
3325
|
+
twowayEventHandler = twowayEventHandlerFunction(binding.stateName, binding.propName, binding.statePathName, binding.inFilters, valueGetter, isOccurrence);
|
|
3210
3326
|
handlerByHandlerKey.set(key, twowayEventHandler);
|
|
3211
3327
|
}
|
|
3212
3328
|
binding.node.addEventListener(eventName, twowayEventHandler);
|
|
@@ -3228,7 +3344,7 @@ function detachTwowayEventHandler(binding) {
|
|
|
3228
3344
|
if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf('ro') === -1) {
|
|
3229
3345
|
const eventName = getEventName(binding);
|
|
3230
3346
|
const valueGetter = getValueGetter(binding);
|
|
3231
|
-
const key = getHandlerKey(binding, eventName, valueGetter !== null);
|
|
3347
|
+
const key = getHandlerKey(binding, eventName, valueGetter !== null, isOccurrenceProperty(binding));
|
|
3232
3348
|
const twowayEventHandler = handlerByHandlerKey.get(key);
|
|
3233
3349
|
if (typeof twowayEventHandler === "undefined") {
|
|
3234
3350
|
return;
|
|
@@ -6986,7 +7102,7 @@ async function buildBindings(root) {
|
|
|
6986
7102
|
}
|
|
6987
7103
|
}
|
|
6988
7104
|
|
|
6989
|
-
var version = "1.
|
|
7105
|
+
var version = "1.25.0";
|
|
6990
7106
|
var pkg = {
|
|
6991
7107
|
version: version};
|
|
6992
7108
|
|
|
@@ -9854,6 +9970,108 @@ function setSwapInfoByAddress(address, swapInfo) {
|
|
|
9854
9970
|
}
|
|
9855
9971
|
}
|
|
9856
9972
|
|
|
9973
|
+
/**
|
|
9974
|
+
* topologicalRank.ts — 依存グラフ(パス単位)のトポロジカル順位。
|
|
9975
|
+
*
|
|
9976
|
+
* 依存ウォークは list → list.* を展開するために途中でリスト実体を読む。この読み取りが
|
|
9977
|
+
* 正しい値を返すには「そのパスの入力(先行パス)がすべて dirty 化済み」である必要がある。
|
|
9978
|
+
* DFS ではダイヤモンド依存で片腕しか dirty 化していない段階で合流点を評価してしまうため、
|
|
9979
|
+
* パス単位の rank(= 最長経路長)を先に求め、rank の昇順で訪問する。
|
|
9980
|
+
*
|
|
9981
|
+
* rank の定義から、辺 (u → v) が存在すれば必ず rank(u) < rank(v) となる。したがって
|
|
9982
|
+
* rank r のバケットを処理する時点で rank < r のパスはすべて訪問(dirty 化)済みであり、
|
|
9983
|
+
* 同じバケット内のパス同士は互いに先行関係を持たない。
|
|
9984
|
+
*
|
|
9985
|
+
* 値を読まないグラフ走査なので、ウォーク 1 回あたりの追加コストは実測で誤差に
|
|
9986
|
+
* 収まる(メモ化しても差が出なかったため、キャッシュは持たない)。
|
|
9987
|
+
*/
|
|
9988
|
+
function getTopologicalRanks(startPath, staticMap, dynamicMap, maxDepth) {
|
|
9989
|
+
// 1) startPath から到達可能なパス部分グラフと入次数を求める(値は一切読まない)
|
|
9990
|
+
const adjacency = new Map();
|
|
9991
|
+
const inDegree = new Map();
|
|
9992
|
+
const pending = [startPath];
|
|
9993
|
+
inDegree.set(startPath, 0);
|
|
9994
|
+
while (pending.length > 0) {
|
|
9995
|
+
const path = pending.pop();
|
|
9996
|
+
if (adjacency.has(path)) {
|
|
9997
|
+
continue;
|
|
9998
|
+
}
|
|
9999
|
+
const staticDeps = staticMap.get(path);
|
|
10000
|
+
const dynamicDeps = dynamicMap.get(path);
|
|
10001
|
+
let deps;
|
|
10002
|
+
if (staticDeps === undefined) {
|
|
10003
|
+
deps = dynamicDeps ?? [];
|
|
10004
|
+
}
|
|
10005
|
+
else if (dynamicDeps === undefined) {
|
|
10006
|
+
deps = staticDeps;
|
|
10007
|
+
}
|
|
10008
|
+
else {
|
|
10009
|
+
deps = staticDeps.concat(dynamicDeps);
|
|
10010
|
+
}
|
|
10011
|
+
adjacency.set(path, deps);
|
|
10012
|
+
for (let i = 0; i < deps.length; i++) {
|
|
10013
|
+
const dep = deps[i];
|
|
10014
|
+
inDegree.set(dep, (inDegree.get(dep) ?? 0) + 1);
|
|
10015
|
+
if (!adjacency.has(dep)) {
|
|
10016
|
+
pending.push(dep);
|
|
10017
|
+
}
|
|
10018
|
+
}
|
|
10019
|
+
}
|
|
10020
|
+
// 2) Kahn 法。rank は最長経路長(rank[v] = max(rank[u]) + 1)。
|
|
10021
|
+
// 入次数が 0 に落ちて queue に入ったパスだけが「確定」で、緩和の途中で
|
|
10022
|
+
// 暫定値が入っただけのパス(= 循環の一部)は確定扱いにしない。
|
|
10023
|
+
const ranks = new Map();
|
|
10024
|
+
const settled = new Set();
|
|
10025
|
+
const queue = [];
|
|
10026
|
+
for (const [path, degree] of inDegree) {
|
|
10027
|
+
if (degree === 0) {
|
|
10028
|
+
ranks.set(path, 0);
|
|
10029
|
+
settled.add(path);
|
|
10030
|
+
queue.push(path);
|
|
10031
|
+
}
|
|
10032
|
+
}
|
|
10033
|
+
for (let i = 0; i < queue.length; i++) {
|
|
10034
|
+
const path = queue[i];
|
|
10035
|
+
const nextRank = ranks.get(path) + 1;
|
|
10036
|
+
if (nextRank > maxDepth) {
|
|
10037
|
+
raiseError(`Maximum dependency depth of ${maxDepth} exceeded. Possible circular dependency detected at path: ${path}`);
|
|
10038
|
+
}
|
|
10039
|
+
const deps = adjacency.get(path);
|
|
10040
|
+
for (let j = 0; j < deps.length; j++) {
|
|
10041
|
+
const dep = deps[j];
|
|
10042
|
+
if (nextRank > (ranks.get(dep) ?? -1)) {
|
|
10043
|
+
ranks.set(dep, nextRank);
|
|
10044
|
+
}
|
|
10045
|
+
const remaining = inDegree.get(dep) - 1;
|
|
10046
|
+
inDegree.set(dep, remaining);
|
|
10047
|
+
if (remaining === 0) {
|
|
10048
|
+
settled.add(dep);
|
|
10049
|
+
queue.push(dep);
|
|
10050
|
+
}
|
|
10051
|
+
}
|
|
10052
|
+
}
|
|
10053
|
+
// 3) 循環に含まれるパスは rank が決まらない(入次数が 0 に落ちない)。
|
|
10054
|
+
// そもそも正しい評価順が存在しないので、順序保証を諦めて確定済みの
|
|
10055
|
+
// 最大 rank の次にまとめる。打ち切りは従来どおり visited が担う。
|
|
10056
|
+
// 暫定値が残っていると確定パスとの前後関係を誤って表すため、必ず上書きする。
|
|
10057
|
+
if (settled.size !== adjacency.size) {
|
|
10058
|
+
let maxRank = -1;
|
|
10059
|
+
for (const path of settled) {
|
|
10060
|
+
const rank = ranks.get(path);
|
|
10061
|
+
if (rank > maxRank) {
|
|
10062
|
+
maxRank = rank;
|
|
10063
|
+
}
|
|
10064
|
+
}
|
|
10065
|
+
const cycleRank = maxRank + 1;
|
|
10066
|
+
for (const path of adjacency.keys()) {
|
|
10067
|
+
if (!settled.has(path)) {
|
|
10068
|
+
ranks.set(path, cycleRank);
|
|
10069
|
+
}
|
|
10070
|
+
}
|
|
10071
|
+
}
|
|
10072
|
+
return ranks;
|
|
10073
|
+
}
|
|
10074
|
+
|
|
9857
10075
|
const MAX_DEPENDENCY_DEPTH = 1000;
|
|
9858
10076
|
function getIndexes(listDiff, searchType) {
|
|
9859
10077
|
switch (searchType) {
|
|
@@ -9960,152 +10178,178 @@ function getMovedRowExpansionPaths(context, wildcardPath, depPathInfo) {
|
|
|
9960
10178
|
return result ?? EMPTY_PATH_INFOS;
|
|
9961
10179
|
}
|
|
9962
10180
|
function _walkDependency(context, startAddress, callback) {
|
|
9963
|
-
|
|
9964
|
-
|
|
9965
|
-
|
|
9966
|
-
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
|
|
10181
|
+
// rank ごとのバケットで訪問する。辺 (u → v) では必ず rank(u) < rank(v) なので、
|
|
10182
|
+
// バケット r を処理する時点で rank < r のパスは全て dirty 化済みになる
|
|
10183
|
+
// = ここでリスト実体を読んでも入力が揃っている(topologicalRank.ts 参照)。
|
|
10184
|
+
const buckets = [];
|
|
10185
|
+
const ranks = context.ranks;
|
|
10186
|
+
const enqueue = (address, minRank) => {
|
|
10187
|
+
let rank = ranks.get(address.pathInfo.path) ?? minRank;
|
|
10188
|
+
if (rank < minRank) {
|
|
10189
|
+
// 循環など rank が先行関係を表せないケース。現在のバケットに載せて
|
|
10190
|
+
// 同一ループ内で処理する(打ち切りは visited が担う)。
|
|
10191
|
+
rank = minRank;
|
|
10192
|
+
}
|
|
10193
|
+
(buckets[rank] ??= []).push(address);
|
|
10194
|
+
};
|
|
10195
|
+
enqueue(startAddress, 0);
|
|
10196
|
+
// 依存アドレスを収集するための一時バッファ(アドレスごとに使い回す)
|
|
10197
|
+
const nextEntries = [];
|
|
10198
|
+
for (let rank = 0; rank < buckets.length; rank++) {
|
|
10199
|
+
const bucket = buckets[rank];
|
|
10200
|
+
if (bucket === undefined) {
|
|
9970
10201
|
continue;
|
|
9971
10202
|
}
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
|
|
10203
|
+
// 同一バケットへの push(循環時)で伸びるため length は都度読む
|
|
10204
|
+
for (let cursor = 0; cursor < bucket.length; cursor++) {
|
|
10205
|
+
const address = bucket[cursor];
|
|
10206
|
+
if (context.visited.has(address)) {
|
|
10207
|
+
continue;
|
|
10208
|
+
}
|
|
10209
|
+
context.visited.add(address);
|
|
10210
|
+
callback(address);
|
|
10211
|
+
nextEntries.length = 0;
|
|
10212
|
+
_collectDependencies(context, address, nextEntries);
|
|
10213
|
+
for (let i = 0; i < nextEntries.length; i++) {
|
|
10214
|
+
enqueue(nextEntries[i], rank + 1);
|
|
10215
|
+
}
|
|
10216
|
+
}
|
|
10217
|
+
}
|
|
10218
|
+
}
|
|
10219
|
+
/**
|
|
10220
|
+
* address の依存アドレスを nextEntries に集め、context.result にも登録する。
|
|
10221
|
+
* リスト展開(list → list.*)と動的依存のワイルドカード展開はここで値を読むが、
|
|
10222
|
+
* 呼び出し元がトポロジカル順を保証しているため入力は揃っている。
|
|
10223
|
+
*/
|
|
10224
|
+
function _collectDependencies(context, address, nextEntries) {
|
|
10225
|
+
const sourcePath = address.pathInfo.path;
|
|
10226
|
+
/**
|
|
10227
|
+
* パスから依存関係をたどる
|
|
10228
|
+
* users.*.name <= users.* <= users
|
|
10229
|
+
* ただし、users がリストであれば users.* の依存関係は展開する
|
|
10230
|
+
*/
|
|
10231
|
+
const staticDeps = context.staticMap.get(sourcePath);
|
|
10232
|
+
if (staticDeps) {
|
|
10233
|
+
for (const dep of staticDeps) {
|
|
10234
|
+
const depPathInfo = getPathInfo(dep);
|
|
10235
|
+
if (context.listPathSet.has(sourcePath) && depPathInfo.lastSegment === WILDCARD) {
|
|
10236
|
+
//expand indexes
|
|
10237
|
+
const newValue = context.stateProxy[getByAddressSymbol](address);
|
|
10238
|
+
const absPathInfo = getAbsolutePathInfo(context.stateElement, address.pathInfo);
|
|
10239
|
+
const absAddress = createAbsoluteStateAddress(absPathInfo, address.listIndex);
|
|
10240
|
+
const lastValue = getLastListValueByAbsoluteStateAddress(absAddress);
|
|
10241
|
+
const listDiff = createListDiff(address.listIndex, lastValue, newValue);
|
|
10242
|
+
const selection = selectExpansionIndexes(context, sourcePath, lastValue, newValue, listDiff);
|
|
10243
|
+
for (const listIndex of selection.fullRows) {
|
|
10244
|
+
const depAddress = createStateAddress(depPathInfo, listIndex);
|
|
10245
|
+
context.result.add(depAddress);
|
|
10246
|
+
nextEntries.push(depAddress);
|
|
10247
|
+
}
|
|
10248
|
+
if (selection.movedRows !== null) {
|
|
10249
|
+
const movedPathInfos = getMovedRowExpansionPaths(context, dep, depPathInfo);
|
|
10250
|
+
if (movedPathInfos === null) {
|
|
10251
|
+
// ネスト配下に index 依存 getter: 安全側で行全体を展開(従来挙動)
|
|
10252
|
+
for (const listIndex of selection.movedRows) {
|
|
10253
|
+
const depAddress = createStateAddress(depPathInfo, listIndex);
|
|
10254
|
+
context.result.add(depAddress);
|
|
10255
|
+
nextEntries.push(depAddress);
|
|
10256
|
+
}
|
|
9999
10257
|
}
|
|
10000
|
-
if (
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
const depAddress = createStateAddress(depPathInfo, listIndex);
|
|
10258
|
+
else if (movedPathInfos.length > 0) {
|
|
10259
|
+
// 位置のみ変わった行は index 依存 getter のパスだけを展開する
|
|
10260
|
+
for (const listIndex of selection.movedRows) {
|
|
10261
|
+
for (const pathInfo of movedPathInfos) {
|
|
10262
|
+
const depAddress = createStateAddress(pathInfo, listIndex);
|
|
10006
10263
|
context.result.add(depAddress);
|
|
10007
|
-
nextEntries.push(
|
|
10008
|
-
}
|
|
10009
|
-
}
|
|
10010
|
-
else if (movedPathInfos.length > 0) {
|
|
10011
|
-
// 位置のみ変わった行は index 依存 getter のパスだけを展開する
|
|
10012
|
-
for (const listIndex of selection.movedRows) {
|
|
10013
|
-
for (const pathInfo of movedPathInfos) {
|
|
10014
|
-
const depAddress = createStateAddress(pathInfo, listIndex);
|
|
10015
|
-
context.result.add(depAddress);
|
|
10016
|
-
nextEntries.push({ address: depAddress, depth: nextDepth });
|
|
10017
|
-
}
|
|
10264
|
+
nextEntries.push(depAddress);
|
|
10018
10265
|
}
|
|
10019
10266
|
}
|
|
10020
|
-
// movedPathInfos が空: index を読む getter が subtree に無い =
|
|
10021
|
-
// 位置のみ変わった行の値は不変。展開・dirty 化とも不要。
|
|
10022
10267
|
}
|
|
10023
|
-
|
|
10024
|
-
|
|
10025
|
-
const depAddress = createStateAddress(depPathInfo, address.listIndex);
|
|
10026
|
-
context.result.add(depAddress);
|
|
10027
|
-
nextEntries.push({ address: depAddress, depth: nextDepth });
|
|
10268
|
+
// movedPathInfos が空: index を読む getter が subtree に無い =
|
|
10269
|
+
// 位置のみ変わった行の値は不変。展開・dirty 化とも不要。
|
|
10028
10270
|
}
|
|
10029
10271
|
}
|
|
10272
|
+
else {
|
|
10273
|
+
const depAddress = createStateAddress(depPathInfo, address.listIndex);
|
|
10274
|
+
context.result.add(depAddress);
|
|
10275
|
+
nextEntries.push(depAddress);
|
|
10276
|
+
}
|
|
10030
10277
|
}
|
|
10031
|
-
|
|
10032
|
-
|
|
10033
|
-
|
|
10034
|
-
|
|
10035
|
-
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
raiseError(`Cannot expand dynamic dependency with wildcard for non-list address: ${address.pathInfo.path}`);
|
|
10065
|
-
}
|
|
10066
|
-
listIndex = address.listIndex.at(wildcardLen - 1);
|
|
10067
|
-
}
|
|
10068
|
-
else {
|
|
10069
|
-
// selectedIndex => items.*.selected
|
|
10070
|
-
// 同じ親を持たない場合はnullから開始
|
|
10071
|
-
listIndex = null;
|
|
10072
|
-
}
|
|
10073
|
-
const expandContext = {
|
|
10074
|
-
stateElement: context.stateElement,
|
|
10075
|
-
targetListIndexes: [],
|
|
10076
|
-
wildcardPaths: depPathInfo.wildcardPaths,
|
|
10077
|
-
wildcardParentPaths: depPathInfo.wildcardParentPaths,
|
|
10078
|
-
stateProxy: context.stateProxy,
|
|
10079
|
-
searchType: context.searchType,
|
|
10080
|
-
};
|
|
10081
|
-
_walkExpandWildcard(expandContext, wildcardLen, listIndex);
|
|
10082
|
-
listIndexes.push(...expandContext.targetListIndexes);
|
|
10083
|
-
}
|
|
10084
|
-
else {
|
|
10085
|
-
// products.*.price => products.*.tax
|
|
10086
|
-
// ワイルドカードを含む同じ親(products.*)を持つので、リストインデックスは引き継ぐ
|
|
10278
|
+
}
|
|
10279
|
+
/**
|
|
10280
|
+
* 動的依存関係をたどる
|
|
10281
|
+
* 動的依存関係は、getterの実行時に決定される
|
|
10282
|
+
*
|
|
10283
|
+
* source, target
|
|
10284
|
+
*
|
|
10285
|
+
* products.*.price => products.*.tax
|
|
10286
|
+
* get "products.*.tax"() { return this["products.*.price"] * 0.1; }
|
|
10287
|
+
*
|
|
10288
|
+
* products.*.price => products.summary
|
|
10289
|
+
* get "products.summary"() { return this.$getAll("products.*.price", []).reduce(sum); }
|
|
10290
|
+
*
|
|
10291
|
+
* categories.*.name => categories.*.products.*.categoryName
|
|
10292
|
+
* get "categories.*.products.*.categoryName"() { return this["categories.*.name"]; }
|
|
10293
|
+
*/
|
|
10294
|
+
const dynamicDeps = context.dynamicMap.get(sourcePath);
|
|
10295
|
+
if (dynamicDeps) {
|
|
10296
|
+
for (const dep of dynamicDeps) {
|
|
10297
|
+
const depPathInfo = getPathInfo(dep);
|
|
10298
|
+
const listIndexes = [];
|
|
10299
|
+
if (depPathInfo.wildcardCount > 0) {
|
|
10300
|
+
// ワイルドカードを含む依存関係の処理
|
|
10301
|
+
// 同じ親を持つかをパスの集合積で判定する
|
|
10302
|
+
// polyfills.tsにてSetのintersectionメソッドを定義している
|
|
10303
|
+
const wildcardLen = calcWildcardLen(address.pathInfo, depPathInfo);
|
|
10304
|
+
const expandable = (depPathInfo.wildcardCount - wildcardLen) >= 1;
|
|
10305
|
+
if (expandable) {
|
|
10306
|
+
let listIndex;
|
|
10307
|
+
if (wildcardLen > 0) {
|
|
10308
|
+
// categories.*.name => categories.*.products.*.categoryName
|
|
10309
|
+
// ワイルドカードを含む同じ親(products.*)を持つのが、
|
|
10310
|
+
// さらに下位にワイルドカードがあるので展開する
|
|
10087
10311
|
if (address.listIndex === null) {
|
|
10088
10312
|
raiseError(`Cannot expand dynamic dependency with wildcard for non-list address: ${address.pathInfo.path}`);
|
|
10089
10313
|
}
|
|
10090
|
-
|
|
10091
|
-
listIndexes.push(listIndex);
|
|
10314
|
+
listIndex = address.listIndex.at(wildcardLen - 1);
|
|
10092
10315
|
}
|
|
10316
|
+
else {
|
|
10317
|
+
// selectedIndex => items.*.selected
|
|
10318
|
+
// 同じ親を持たない場合はnullから開始
|
|
10319
|
+
listIndex = null;
|
|
10320
|
+
}
|
|
10321
|
+
const expandContext = {
|
|
10322
|
+
stateName: context.stateName,
|
|
10323
|
+
stateElement: context.stateElement,
|
|
10324
|
+
targetListIndexes: [],
|
|
10325
|
+
wildcardPaths: depPathInfo.wildcardPaths,
|
|
10326
|
+
wildcardParentPaths: depPathInfo.wildcardParentPaths,
|
|
10327
|
+
stateProxy: context.stateProxy,
|
|
10328
|
+
searchType: context.searchType,
|
|
10329
|
+
};
|
|
10330
|
+
_walkExpandWildcard(expandContext, wildcardLen, listIndex);
|
|
10331
|
+
listIndexes.push(...expandContext.targetListIndexes);
|
|
10093
10332
|
}
|
|
10094
10333
|
else {
|
|
10095
|
-
// products.*.
|
|
10096
|
-
//
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
const
|
|
10101
|
-
|
|
10102
|
-
nextEntries.push({ address: depAddress, depth: nextDepth });
|
|
10334
|
+
// products.*.price => products.*.tax
|
|
10335
|
+
// ワイルドカードを含む同じ親(products.*)を持つので、リストインデックスは引き継ぐ
|
|
10336
|
+
if (address.listIndex === null) {
|
|
10337
|
+
raiseError(`Cannot expand dynamic dependency with wildcard for non-list address: ${address.pathInfo.path}`);
|
|
10338
|
+
}
|
|
10339
|
+
const listIndex = address.listIndex.at(wildcardLen - 1);
|
|
10340
|
+
listIndexes.push(listIndex);
|
|
10103
10341
|
}
|
|
10104
10342
|
}
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10343
|
+
else {
|
|
10344
|
+
// products.*.tax => currentTaxRate
|
|
10345
|
+
// 同じ親を持たないので、リストインデックスはnull
|
|
10346
|
+
listIndexes.push(null);
|
|
10347
|
+
}
|
|
10348
|
+
for (const listIndex of listIndexes) {
|
|
10349
|
+
const depAddress = createStateAddress(depPathInfo, listIndex);
|
|
10350
|
+
context.result.add(depAddress);
|
|
10351
|
+
nextEntries.push(depAddress);
|
|
10352
|
+
}
|
|
10109
10353
|
}
|
|
10110
10354
|
}
|
|
10111
10355
|
}
|
|
@@ -10119,7 +10363,12 @@ function walkDependency(stateName, stateElement, startAddress, staticDependency,
|
|
|
10119
10363
|
callback(startAddress);
|
|
10120
10364
|
return [];
|
|
10121
10365
|
}
|
|
10366
|
+
// パス単位のトポロジカル順位。値を一切読まずに求まり、依存グラフは追記のみで
|
|
10367
|
+
// 成長するため epoch でメモ化される(topologicalRank.ts)。
|
|
10368
|
+
const ranks = getTopologicalRanks(startPath, staticDependency, dynamicDependency, MAX_DEPENDENCY_DEPTH);
|
|
10122
10369
|
const context = {
|
|
10370
|
+
ranks: ranks,
|
|
10371
|
+
stateName: stateName,
|
|
10123
10372
|
stateElement: stateElement,
|
|
10124
10373
|
staticMap: staticDependency,
|
|
10125
10374
|
dynamicMap: dynamicDependency,
|
|
@@ -10258,6 +10507,10 @@ function _setByAddressWithSwap(target, address, absAddress, value, receiver, han
|
|
|
10258
10507
|
function setByAddress(target, address, value, receiver, handler) {
|
|
10259
10508
|
const stateElement = handler.stateElement;
|
|
10260
10509
|
const path = address.pathInfo.path;
|
|
10510
|
+
// occurrence(wc-bindable の `semantics: "event"`)由来の書き込みは、同値でも
|
|
10511
|
+
// 「もう一度起きた」ことを落としてはならないため same-value guard を 1 回だけ飛ばす。
|
|
10512
|
+
// トークンはここで消費されるので、この write の内側で走る他の書き込みには波及しない。
|
|
10513
|
+
const skipSameValueGuard = consumeOccurrenceWrite();
|
|
10261
10514
|
// --- fast path: 宣言済み getter/setter でも swap 対象でもない、親を持つ葉パス ---
|
|
10262
10515
|
// 従来は same-value guard の値読み・hasByAddress・実書き込みがそれぞれ親チェーンを
|
|
10263
10516
|
// 解決していた(キャッシュヒットでも getByAddress 呼び出しの固定費 ×3)。
|
|
@@ -10274,7 +10527,7 @@ function setByAddress(target, address, value, receiver, handler) {
|
|
|
10274
10527
|
: lastSegment;
|
|
10275
10528
|
let devOldValue;
|
|
10276
10529
|
let devHasOldValue = false;
|
|
10277
|
-
if (config.sameValueGuard && (value === null || typeof value !== "object")) {
|
|
10530
|
+
if (!skipSameValueGuard && config.sameValueGuard && (value === null || typeof value !== "object")) {
|
|
10278
10531
|
// hasByAddress と同じ「初期化済みスロットか」判定(undefined 格納と未初期化を区別)
|
|
10279
10532
|
const has = key !== undefined && key in parentValue;
|
|
10280
10533
|
const oldValue = key !== undefined ? parentValue[key] : undefined;
|
|
@@ -10334,7 +10587,7 @@ function setByAddress(target, address, value, receiver, handler) {
|
|
|
10334
10587
|
// (参照型のために追加の get はしない — protocol §4.2)
|
|
10335
10588
|
let devOldValue;
|
|
10336
10589
|
let devHasOldValue = false;
|
|
10337
|
-
if (config.sameValueGuard && (value === null || typeof value !== "object")) {
|
|
10590
|
+
if (!skipSameValueGuard && config.sameValueGuard && (value === null || typeof value !== "object")) {
|
|
10338
10591
|
const oldValue = getByAddress(target, address, receiver, handler);
|
|
10339
10592
|
if (hasByAddress(target, address, receiver, handler) && Object.is(oldValue, value)) {
|
|
10340
10593
|
return true;
|
|
@@ -10716,7 +10969,7 @@ function updatedCallback(target, refs, receiver, handler) {
|
|
|
10716
10969
|
* setLoopContext.ts
|
|
10717
10970
|
*
|
|
10718
10971
|
* StateClassの内部APIとして、ループコンテキスト(ILoopContext)を一時的に設定し、
|
|
10719
|
-
*
|
|
10972
|
+
* 指定したコールバックをそのスコープ内で実行するための関数です。
|
|
10720
10973
|
*
|
|
10721
10974
|
* 主な役割:
|
|
10722
10975
|
* - handler.loopContextにループコンテキストを一時的に設定
|
|
@@ -10727,9 +10980,26 @@ function updatedCallback(target, refs, receiver, handler) {
|
|
|
10727
10980
|
* 設計ポイント:
|
|
10728
10981
|
* - ループバインディングや多重ループ時のスコープ管理を安全に行う
|
|
10729
10982
|
* - finallyで状態復元を保証し、例外発生時も安全
|
|
10730
|
-
*
|
|
10983
|
+
*
|
|
10984
|
+
* **スコープは同期である(重要)**:
|
|
10985
|
+
* push/pop は `callback()` の同期リターンで完結する。callback が Promise を返した
|
|
10986
|
+
* 場合、finally はその Promise が settle する *前* に走るため、await を跨いだ先では
|
|
10987
|
+
* ループコンテキストは既に外れている。したがって async なハンドラが await の後に
|
|
10988
|
+
* `$1` や wildcard パス(`items.*.id`)を触ると raiseError になる。
|
|
10989
|
+
*
|
|
10990
|
+
* これは silent な取り違えではなく loud な失敗であり、意図した挙動である
|
|
10991
|
+
* (特性化テスト: `__tests__/poc.asyncOnLoopContext.test.ts`)。await の後に行位置が
|
|
10992
|
+
* 必要な場合は、ハンドラ引数で受け取った listIndexes を
|
|
10993
|
+
* `$resolve(path, indexes, value?)` に渡すこと。listIndexes は素の数値配列なので
|
|
10994
|
+
* await を跨いでも安全に持ち回せる。
|
|
10995
|
+
*
|
|
10996
|
+
* 補足: かつて `setLoopContextAsync` という変種が存在したが、実体は
|
|
10997
|
+
* `await _setLoopContext(...)`(= finally が既に走った後の Promise を await するだけ)
|
|
10998
|
+
* であり、名前が示唆する「コンテキストを await 跨ぎで保持する」挙動は持っていなかった。
|
|
10999
|
+
* production の呼び出し元も無かったため削除した。同等の機能が必要になった場合は、
|
|
11000
|
+
* 「名前どおりに動く」実装を新規に起こすこと。
|
|
10731
11001
|
*/
|
|
10732
|
-
function
|
|
11002
|
+
function setLoopContext(handler, loopContext, callback) {
|
|
10733
11003
|
if (typeof handler.loopContext !== "undefined") {
|
|
10734
11004
|
raiseError('already in loop context');
|
|
10735
11005
|
}
|
|
@@ -10747,12 +11017,6 @@ function _setLoopContext(handler, loopContext, callback) {
|
|
|
10747
11017
|
handler.clearLoopContext();
|
|
10748
11018
|
}
|
|
10749
11019
|
}
|
|
10750
|
-
function setLoopContext(handler, loopContext, callback) {
|
|
10751
|
-
return _setLoopContext(handler, loopContext, callback);
|
|
10752
|
-
}
|
|
10753
|
-
async function setLoopContextAsync(handler, loopContext, callback) {
|
|
10754
|
-
return await _setLoopContext(handler, loopContext, callback);
|
|
10755
|
-
}
|
|
10756
11020
|
|
|
10757
11021
|
/**
|
|
10758
11022
|
* get.ts
|
|
@@ -10870,12 +11134,6 @@ function get(target, prop, receiver, handler) {
|
|
|
10870
11134
|
}
|
|
10871
11135
|
let api;
|
|
10872
11136
|
switch (prop) {
|
|
10873
|
-
case setLoopContextAsyncSymbol: {
|
|
10874
|
-
api = (loopContext, callback = async () => { }) => {
|
|
10875
|
-
return setLoopContextAsync(handler, loopContext, callback);
|
|
10876
|
-
};
|
|
10877
|
-
break;
|
|
10878
|
-
}
|
|
10879
11137
|
case setLoopContextSymbol: {
|
|
10880
11138
|
api = (loopContext, callback = () => { }) => {
|
|
10881
11139
|
return setLoopContext(handler, loopContext, callback);
|