@wcstack/state 1.24.0 → 1.26.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 +128 -10
- package/README.md +128 -10
- package/dist/auto.min.js +2 -3
- package/dist/auto.min.js.map +1 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.esm.js +1802 -484
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.esm.js +4 -0
- package/dist/wcs-manifest.json +2 -0
- package/package.json +1 -1
- package/dist/auto.js +0 -3
- package/dist/index.esm.min.js +0 -2
- package/dist/index.esm.min.js.map +0 -1
package/dist/index.esm.js
CHANGED
|
@@ -158,11 +158,13 @@ const STATE_DISCONNECTED_CALLBACK_NAME = "$disconnectedCallback";
|
|
|
158
158
|
const STATE_UPDATED_CALLBACK_NAME = "$updatedCallback";
|
|
159
159
|
const WEBCOMPONENT_STATE_READY_CALLBACK_NAME = "$stateReadyCallback";
|
|
160
160
|
const STATE_BINDABLES_NAME = "$bindables";
|
|
161
|
+
const STATE_COMMANDS_NAME = "$commands";
|
|
161
162
|
const STATE_COMMAND_TOKENS_NAME = "$commandTokens";
|
|
162
163
|
const STATE_COMMAND_NAMESPACE_NAME = "$command";
|
|
163
164
|
const STATE_EVENT_TOKENS_NAME = "$eventTokens";
|
|
164
165
|
const STATE_ON_NAME = "$on";
|
|
165
166
|
const STATE_STREAMS_NAME = "$streams";
|
|
167
|
+
const STATE_LIST_KEYS_NAME = "$listKeys";
|
|
166
168
|
const STATE_STREAM_STATUS_NAMESPACE_NAME = "$streamStatus";
|
|
167
169
|
const STATE_STREAM_ERROR_NAMESPACE_NAME = "$streamError";
|
|
168
170
|
const DCC_DEFINITION_ATTRIBUTE = "data-wc-definition";
|
|
@@ -612,15 +614,6 @@ function getBindingsByNode(node) {
|
|
|
612
614
|
function setBindingsByNode(node, bindings) {
|
|
613
615
|
bindingsByNode.set(node, bindings);
|
|
614
616
|
}
|
|
615
|
-
function addBindingByNode(node, binding) {
|
|
616
|
-
const bindings = getBindingsByNode(node);
|
|
617
|
-
if (bindings === null) {
|
|
618
|
-
setBindingsByNode(node, [binding]);
|
|
619
|
-
}
|
|
620
|
-
else {
|
|
621
|
-
bindings.push(binding);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
617
|
|
|
625
618
|
const STRUCTURAL_BINDING_TYPE_SET = new Set([
|
|
626
619
|
"if",
|
|
@@ -2058,6 +2051,56 @@ function calcWildcardLen(pathInfo, targetPathInfo) {
|
|
|
2058
2051
|
return len;
|
|
2059
2052
|
}
|
|
2060
2053
|
|
|
2054
|
+
/**
|
|
2055
|
+
* list/wildcardLevel.ts
|
|
2056
|
+
*
|
|
2057
|
+
* 「パス上のワイルドカード位置」→「listIndex チェーン上の段」の変換を 1 箇所に集める。
|
|
2058
|
+
*
|
|
2059
|
+
* チェーンの長さは常に `Δ + W`(W = そのパスの wildcardCount、Δ = そのスコープの
|
|
2060
|
+
* base 深さ)である。通常の state スコープは Δ=0 なので「位置 i = 段 i」で済んでいたが、
|
|
2061
|
+
* `bind-component` の子スコープがホストの `for` の内側にいる場合は Δ>0 になる
|
|
2062
|
+
* (docs/state-bind-component-nested-for-design.md)。
|
|
2063
|
+
*
|
|
2064
|
+
* そこで**先頭ではなく末尾を基準に数える**。`IListIndex.at()` は負値を受けるので:
|
|
2065
|
+
*
|
|
2066
|
+
* at(i) → at(i - W) // listIndexes[(Δ+W) + (i-W)] = listIndexes[Δ+i]
|
|
2067
|
+
*
|
|
2068
|
+
* Δ=0 のときは両者が同じ要素を指すため、この書き換えは既存スコープに対して
|
|
2069
|
+
* 意味論を変えない。Δ の値を呼び出し側へ配管する必要も無い。
|
|
2070
|
+
*/
|
|
2071
|
+
/**
|
|
2072
|
+
* ワイルドカード位置 `wildcardPos`(先頭から 0 始まり)に対応する listIndex を返す。
|
|
2073
|
+
* `wildcardCount` は `wildcardPos` が属するパスのワイルドカード総数。
|
|
2074
|
+
*
|
|
2075
|
+
* 範囲外(`wildcardPos >= wildcardCount`)は null。Δ=0 では `at(pos)` が
|
|
2076
|
+
* チェーン長を超えて null を返していたのと同じ結果になる。**このガードは必須**で、
|
|
2077
|
+
* 落とすと「1 段ループの中で `$2` を読む」が黙って `$1` を返す
|
|
2078
|
+
* (末尾起点では `at(1-1)=at(0)` に化けるため)。
|
|
2079
|
+
*/
|
|
2080
|
+
function listIndexAtWildcard(listIndex, wildcardPos, wildcardCount) {
|
|
2081
|
+
if (wildcardPos < 0 || wildcardPos >= wildcardCount) {
|
|
2082
|
+
return null;
|
|
2083
|
+
}
|
|
2084
|
+
return listIndex.at(wildcardPos - wildcardCount);
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* ユーザーランドへ渡すインデックス列。チェーンの先頭 Δ 段(base)を落とし、
|
|
2088
|
+
* **そのスコープ自身のループ分だけ**にする。
|
|
2089
|
+
*
|
|
2090
|
+
* コンポーネントの作者は、自分がリストの中に置かれるかどうかを知らずに書く。
|
|
2091
|
+
* `$1` や `onClick(event, index)` の意味が設置場所で変わってはいけないので、
|
|
2092
|
+
* Δ は境界の内側に閉じ込める。`$resolve(path, indexes)` は台帳の配列位置で
|
|
2093
|
+
* 引くため、ここで返した列がそのまま往復で使える。
|
|
2094
|
+
*/
|
|
2095
|
+
function getScopedIndexes(listIndex, wildcardCount) {
|
|
2096
|
+
// indexes は型上は必須だが、防御的フォールバックを既存挙動として持っている
|
|
2097
|
+
const indexes = listIndex.indexes ?? [];
|
|
2098
|
+
if (indexes.length === wildcardCount) {
|
|
2099
|
+
return indexes;
|
|
2100
|
+
}
|
|
2101
|
+
return indexes.slice(indexes.length - wildcardCount);
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2061
2104
|
const listIndexByBindingInfoByLoopContext = new WeakMap();
|
|
2062
2105
|
function getListIndexByBindingInfo(bindingInfo) {
|
|
2063
2106
|
const loopContext = getLoopContextByNode(bindingInfo.node);
|
|
@@ -2079,7 +2122,7 @@ function getListIndexByBindingInfo(bindingInfo) {
|
|
|
2079
2122
|
try {
|
|
2080
2123
|
const wildcardLen = calcWildcardLen(loopContext.pathInfo, bindingInfo.statePathInfo);
|
|
2081
2124
|
if (wildcardLen > 0) {
|
|
2082
|
-
listIndex = loopContext.listIndex
|
|
2125
|
+
listIndex = listIndexAtWildcard(loopContext.listIndex, wildcardLen - 1, loopContext.pathInfo.wildcardCount);
|
|
2083
2126
|
}
|
|
2084
2127
|
return listIndex;
|
|
2085
2128
|
}
|
|
@@ -2702,7 +2745,8 @@ function attachEventTokenHandler(binding) {
|
|
|
2702
2745
|
const loopContext = getLoopContextByNode(element);
|
|
2703
2746
|
stateElement.createStateAsync("writable", async (state) => {
|
|
2704
2747
|
const results = state[setLoopContextSymbol](loopContext, () => {
|
|
2705
|
-
const indexes = loopContext
|
|
2748
|
+
const indexes = loopContext !== null
|
|
2749
|
+
? getScopedIndexes(loopContext.listIndex, loopContext.pathInfo.wildcardCount) : [];
|
|
2706
2750
|
const token = getOrCreateEventToken(stateElement, tokenName);
|
|
2707
2751
|
return token.emit(state, event, ...indexes);
|
|
2708
2752
|
});
|
|
@@ -2787,7 +2831,8 @@ const stateEventHandlerFunction = (stateName, handlerName, modifiers, statePathI
|
|
|
2787
2831
|
const isCommand = isCommandTokenPath(handlerName);
|
|
2788
2832
|
stateElement.createStateAsync("writable", async (state) => {
|
|
2789
2833
|
const results = state[setLoopContextSymbol](loopContext, () => {
|
|
2790
|
-
const indexes = loopContext
|
|
2834
|
+
const indexes = loopContext !== null
|
|
2835
|
+
? getScopedIndexes(loopContext.listIndex, loopContext.pathInfo.wildcardCount) : [];
|
|
2791
2836
|
if (isCommand) {
|
|
2792
2837
|
// command token を解決して emit。引数はハンドラ呼び出しと同じく (event, ...listIndexes) を透過する。
|
|
2793
2838
|
const token = state[getByAddressSymbol](createStateAddress(statePathInfo, null));
|
|
@@ -3356,6 +3401,358 @@ function detachTwowayEventHandler(binding) {
|
|
|
3356
3401
|
}
|
|
3357
3402
|
}
|
|
3358
3403
|
|
|
3404
|
+
/**
|
|
3405
|
+
* webComponent/baseListIndex.ts
|
|
3406
|
+
*
|
|
3407
|
+
* mapped な `bind-component` の子スコープが「親スコープのどの行の内側にいるか」。
|
|
3408
|
+
*
|
|
3409
|
+
* ホストのコンポーネント要素が親スコープの `for` の中に置かれている場合、
|
|
3410
|
+
* 子スコープは実際には**ネストしたループの内側**にいる。その深さ Δ を表すのが
|
|
3411
|
+
* base listIndex で、子が作る listIndex はすべてこれを親に持つ。
|
|
3412
|
+
* 結果として `groups[i].children` の listIndex 台帳は arity Δ+1 になり、
|
|
3413
|
+
* これは親が `groups.*.children.*` に対して要求するものと同一になる
|
|
3414
|
+
* (台帳 `listIndexesByList` は配列オブジェクト同一性の WeakMap なので、
|
|
3415
|
+
* 1 つの配列につき 1 組しか持てない。親子で同じ組を使うのが唯一の整合手段)。
|
|
3416
|
+
*
|
|
3417
|
+
* 詳細は docs/state-bind-component-nested-for-design.md。
|
|
3418
|
+
*
|
|
3419
|
+
* **キャッシュしてはいけない。** 行 content はプールで再利用されるため、同じ
|
|
3420
|
+
* コンポーネント要素が別の行に付け替わる。要素をキーにした memo は §1.9 で
|
|
3421
|
+
* 踏んだ罠そのもので、再接続後に古い行を指し続ける。
|
|
3422
|
+
* 通常の state(`hasMappedComponentState` が偽)は最初の 1 行で抜けるので、
|
|
3423
|
+
* ホットパスに walk は載らない。
|
|
3424
|
+
*/
|
|
3425
|
+
function getBaseListIndex(stateElement) {
|
|
3426
|
+
if (stateElement == null || stateElement.hasMappedComponentState !== true) {
|
|
3427
|
+
return null;
|
|
3428
|
+
}
|
|
3429
|
+
const component = stateElement.boundComponent;
|
|
3430
|
+
if (component == null) {
|
|
3431
|
+
return null;
|
|
3432
|
+
}
|
|
3433
|
+
return getLoopContextByNode(component)?.listIndex ?? null;
|
|
3434
|
+
}
|
|
3435
|
+
/** base の段数 Δ。base が無ければ 0。 */
|
|
3436
|
+
function getBaseDepth(stateElement) {
|
|
3437
|
+
return getBaseListIndex(stateElement)?.length ?? 0;
|
|
3438
|
+
}
|
|
3439
|
+
/**
|
|
3440
|
+
* リストの行を生成するときの親 listIndex。
|
|
3441
|
+
*
|
|
3442
|
+
* コンテナのアドレスがワイルドカードを持つ(=囲むループがある)ならその listIndex、
|
|
3443
|
+
* 持たない(=そのスコープのトップレベルのリスト)なら base。後者を null のままに
|
|
3444
|
+
* すると、子スコープのリストだけ arity 1 で作られて親の台帳と食い違う。
|
|
3445
|
+
*
|
|
3446
|
+
* **リストの行を作りうる全経路で使うこと。** 既存台帳があれば `createListDiff` は
|
|
3447
|
+
* 再利用するので初期描画では食い違いが見えず、**行を追加したときだけ**
|
|
3448
|
+
* `createListIndex(parentListIndex, i)` が新しい arity で作られて混在する。
|
|
3449
|
+
*/
|
|
3450
|
+
function getListParentListIndex(stateElement, containerListIndex) {
|
|
3451
|
+
return containerListIndex ?? getBaseListIndex(stateElement);
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
const stateElementByWebComponent = new WeakMap();
|
|
3455
|
+
function setStateElementByWebComponent(webComponent, stateName, stateElement) {
|
|
3456
|
+
let stateMap = stateElementByWebComponent.get(webComponent);
|
|
3457
|
+
if (!stateMap) {
|
|
3458
|
+
stateMap = new Map();
|
|
3459
|
+
stateElementByWebComponent.set(webComponent, stateMap);
|
|
3460
|
+
}
|
|
3461
|
+
stateMap.set(stateName, stateElement);
|
|
3462
|
+
}
|
|
3463
|
+
function getStateElementByWebComponent(webComponent, stateName) {
|
|
3464
|
+
const stateMap = stateElementByWebComponent.get(webComponent);
|
|
3465
|
+
if (!stateMap) {
|
|
3466
|
+
return null;
|
|
3467
|
+
}
|
|
3468
|
+
return stateMap.get(stateName) ?? null;
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
const innerMappingByElement = new WeakMap();
|
|
3472
|
+
const outerMappingByElement = new WeakMap();
|
|
3473
|
+
const primaryMappingRuleSetByElement = new WeakMap();
|
|
3474
|
+
const primaryBindingByMappingRule = new WeakMap();
|
|
3475
|
+
function createMappingRuleByBinding(innerState, binding) {
|
|
3476
|
+
const innerPathInfo = getPathInfo(binding.propSegments.slice(1).join(DELIMITER));
|
|
3477
|
+
const innerAbsPathInfo = getAbsolutePathInfo(innerState, innerPathInfo);
|
|
3478
|
+
const outerAbsStateAddress = getAbsoluteStateAddressByBinding(binding);
|
|
3479
|
+
const outerAbsPathInfo = outerAbsStateAddress.absolutePathInfo;
|
|
3480
|
+
return { innerAbsPathInfo, outerAbsPathInfo };
|
|
3481
|
+
}
|
|
3482
|
+
function buildPrimaryMappingRule(webComponent, stateName, bindings) {
|
|
3483
|
+
if (bindings.length === 0) {
|
|
3484
|
+
return;
|
|
3485
|
+
}
|
|
3486
|
+
const innerState = getStateElementByWebComponent(webComponent, stateName);
|
|
3487
|
+
if (innerState === null) {
|
|
3488
|
+
raiseError('State element not found for web component.');
|
|
3489
|
+
}
|
|
3490
|
+
const innerMappingRule = new Map();
|
|
3491
|
+
const outerMappingRule = new Map();
|
|
3492
|
+
for (const binding of bindings) {
|
|
3493
|
+
const mappingRule = createMappingRuleByBinding(innerState, binding);
|
|
3494
|
+
let primaryMappingRuleSet = primaryMappingRuleSetByElement.get(webComponent);
|
|
3495
|
+
if (typeof primaryMappingRuleSet === 'undefined') {
|
|
3496
|
+
primaryMappingRuleSetByElement.set(webComponent, new Set([mappingRule]));
|
|
3497
|
+
}
|
|
3498
|
+
else {
|
|
3499
|
+
primaryMappingRuleSet.add(mappingRule);
|
|
3500
|
+
}
|
|
3501
|
+
const innerAbsPathInfo = mappingRule.innerAbsPathInfo;
|
|
3502
|
+
const outerAbsPathInfo = mappingRule.outerAbsPathInfo;
|
|
3503
|
+
primaryBindingByMappingRule.set(mappingRule, binding);
|
|
3504
|
+
innerMappingRule.set(innerAbsPathInfo, outerAbsPathInfo);
|
|
3505
|
+
outerMappingRule.set(outerAbsPathInfo, innerAbsPathInfo);
|
|
3506
|
+
}
|
|
3507
|
+
innerMappingByElement.set(webComponent, innerMappingRule);
|
|
3508
|
+
outerMappingByElement.set(webComponent, outerMappingRule);
|
|
3509
|
+
}
|
|
3510
|
+
/**
|
|
3511
|
+
* プライマリ規則だけを残して、遅延導出された派生規則の memo を捨てる(§1.9)。
|
|
3512
|
+
*
|
|
3513
|
+
* 派生規則は導出と同時に「親スコープの購読者」を立てる。その購読者は子の切断で
|
|
3514
|
+
* teardown されるが、memo は要素をキーに残り続けるため、再接続後は**導出が二度と
|
|
3515
|
+
* 走らず購読者も張り直されない** — 親がサブパスへ書いても子に届かなくなる。
|
|
3516
|
+
* リスト行の content 再利用で実際に踏む(行を差し替えると、その行の子だけが
|
|
3517
|
+
* 以後の行フィールド書き込みを受け取れない)。
|
|
3518
|
+
*
|
|
3519
|
+
* `buildPrimaryMappingRule` は再バインド時に同じことをしている(台帳を作り直す)。
|
|
3520
|
+
* 再接続では bindWebComponent が走らないので、ここで同じ状態に戻す。
|
|
3521
|
+
*/
|
|
3522
|
+
function resetDerivedMappingRules(webComponent) {
|
|
3523
|
+
const primaryMappingRuleSet = primaryMappingRuleSetByElement.get(webComponent);
|
|
3524
|
+
if (typeof primaryMappingRuleSet === 'undefined') {
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
const innerMappingRule = new Map();
|
|
3528
|
+
const outerMappingRule = new Map();
|
|
3529
|
+
for (const rule of primaryMappingRuleSet) {
|
|
3530
|
+
innerMappingRule.set(rule.innerAbsPathInfo, rule.outerAbsPathInfo);
|
|
3531
|
+
outerMappingRule.set(rule.outerAbsPathInfo, rule.innerAbsPathInfo);
|
|
3532
|
+
}
|
|
3533
|
+
innerMappingByElement.set(webComponent, innerMappingRule);
|
|
3534
|
+
outerMappingByElement.set(webComponent, outerMappingRule);
|
|
3535
|
+
}
|
|
3536
|
+
/**
|
|
3537
|
+
* このコンポーネントに張られたプライマリ規則の**内側パス**を列挙する。
|
|
3538
|
+
*
|
|
3539
|
+
* 切断 → 再接続を跨いだ子(行 content の再利用で起きる)は、切断中に親で起きた変更の
|
|
3540
|
+
* 通知を受け取れていない。再接続時に「束ねているパスを読み直せ」と撃つための入力で、
|
|
3541
|
+
* 何が変わったかは分からないのでプライマリ規則の粒度で丸ごと読み直す(§1.9)。
|
|
3542
|
+
*/
|
|
3543
|
+
function getPrimaryInnerPaths(webComponent) {
|
|
3544
|
+
const primaryMappingRuleSet = primaryMappingRuleSetByElement.get(webComponent);
|
|
3545
|
+
if (typeof primaryMappingRuleSet === 'undefined') {
|
|
3546
|
+
return [];
|
|
3547
|
+
}
|
|
3548
|
+
const paths = [];
|
|
3549
|
+
for (const rule of primaryMappingRuleSet) {
|
|
3550
|
+
paths.push(rule.innerAbsPathInfo.pathInfo.path);
|
|
3551
|
+
}
|
|
3552
|
+
return paths;
|
|
3553
|
+
}
|
|
3554
|
+
/**
|
|
3555
|
+
* 内側のパスを外側のパスへ翻訳する。規則が無ければプライマリ規則から導出する。
|
|
3556
|
+
*
|
|
3557
|
+
* `registerSubscriber` は導出に**副作用を持たせるか**の切り替え。既定(子の read /
|
|
3558
|
+
* write からの呼び出し)では導出した規則を台帳に memo し、対応するバインディングを
|
|
3559
|
+
* 親スコープの購読者として登録する。`false` を渡すと**参照専用**になり、台帳にも
|
|
3560
|
+
* 購読者にも触れない。
|
|
3561
|
+
*
|
|
3562
|
+
* 参照専用が要るのは、バインディング登録の最中(`BindingSession.registerAddress` →
|
|
3563
|
+
* `setPathInfo` / 行の相乗り登録)に翻訳だけしたい場合。ここで購読者登録まで走ると
|
|
3564
|
+
* `session.initialize` がセッション操作の内側から再入する。
|
|
3565
|
+
*
|
|
3566
|
+
* 参照専用の結果を台帳に memo しないのは、後から来た**本物の read が memo に当たって
|
|
3567
|
+
* 購読者登録を永久に飛ばしてしまう**ため。導出のやり直しは初回だけで、以降は本物の
|
|
3568
|
+
* read が張った memo に当たる(行 2 本目以降の登録は先頭行の read が埋めた台帳を引く)。
|
|
3569
|
+
*/
|
|
3570
|
+
function getOuterAbsolutePathInfo(webComponent, innerAbsPathInfo, registerSubscriber = true) {
|
|
3571
|
+
let innerMapping = innerMappingByElement.get(webComponent);
|
|
3572
|
+
if (typeof innerMapping === 'undefined') {
|
|
3573
|
+
innerMapping = new Map();
|
|
3574
|
+
innerMappingByElement.set(webComponent, innerMapping);
|
|
3575
|
+
}
|
|
3576
|
+
if (innerMapping.has(innerAbsPathInfo)) {
|
|
3577
|
+
return innerMapping.get(innerAbsPathInfo);
|
|
3578
|
+
}
|
|
3579
|
+
let outerMapping = outerMappingByElement.get(webComponent);
|
|
3580
|
+
if (typeof outerMapping === 'undefined') {
|
|
3581
|
+
outerMapping = new Map();
|
|
3582
|
+
outerMappingByElement.set(webComponent, outerMapping);
|
|
3583
|
+
}
|
|
3584
|
+
// 内側からのアクセスの場合、ルールがなければプライマリルールから新たにルールとバインディングを生成する
|
|
3585
|
+
const primaryMappingRuleSet = primaryMappingRuleSetByElement.get(webComponent);
|
|
3586
|
+
if (typeof primaryMappingRuleSet === 'undefined') {
|
|
3587
|
+
// マッピングルールが存在しない場合はnullを返し、ローカル状態へのフォールバックを許可する
|
|
3588
|
+
return null;
|
|
3589
|
+
}
|
|
3590
|
+
let primaryMappingRule = null;
|
|
3591
|
+
for (const currentPrimaryMappingRule of primaryMappingRuleSet) {
|
|
3592
|
+
// innerPathInfoがprimaryMappingRuleのinnerPathInfoを包含しているか
|
|
3593
|
+
if (!innerAbsPathInfo.pathInfo.cumulativePathInfoSet.has(currentPrimaryMappingRule.innerAbsPathInfo.pathInfo)) {
|
|
3594
|
+
continue;
|
|
3595
|
+
}
|
|
3596
|
+
if (currentPrimaryMappingRule.innerAbsPathInfo.pathInfo.segments.length === innerAbsPathInfo.pathInfo.segments.length) {
|
|
3597
|
+
raiseError('Duplicate mapping rule for web component.');
|
|
3598
|
+
}
|
|
3599
|
+
primaryMappingRule = currentPrimaryMappingRule;
|
|
3600
|
+
break;
|
|
3601
|
+
}
|
|
3602
|
+
if (primaryMappingRule === null) {
|
|
3603
|
+
// マッピングルールに一致しない場合はnullを返し、ローカル状態へのフォールバックを許可する
|
|
3604
|
+
return null;
|
|
3605
|
+
}
|
|
3606
|
+
// マッチした残りのパスをouterPathInfoに付与して新たなルールを生成
|
|
3607
|
+
const primaryBinding = primaryBindingByMappingRule.get(primaryMappingRule);
|
|
3608
|
+
/* c8 ignore start */
|
|
3609
|
+
if (typeof primaryBinding === 'undefined') {
|
|
3610
|
+
raiseError('Binding not found for primary mapping rule on web component.');
|
|
3611
|
+
}
|
|
3612
|
+
/* c8 ignore stop */
|
|
3613
|
+
const outerRemainingSegments = innerAbsPathInfo.pathInfo.segments.slice(primaryMappingRule.innerAbsPathInfo.pathInfo.segments.length);
|
|
3614
|
+
const outerSegments = primaryMappingRule.outerAbsPathInfo.pathInfo.segments.concat(outerRemainingSegments);
|
|
3615
|
+
const outerPathInfo = getPathInfo(outerSegments.join(DELIMITER));
|
|
3616
|
+
const rootNode = webComponent.getRootNode();
|
|
3617
|
+
const outerStateElement = getStateElementByName(rootNode, primaryBinding.stateName);
|
|
3618
|
+
if (outerStateElement === null) {
|
|
3619
|
+
raiseError(`State element with name "${primaryBinding.stateName}" not found for web component.`);
|
|
3620
|
+
}
|
|
3621
|
+
const outerAbsPathInfo = getAbsolutePathInfo(outerStateElement, outerPathInfo);
|
|
3622
|
+
if (!registerSubscriber) {
|
|
3623
|
+
// 参照専用: 台帳にも購読者にも触れず、翻訳結果だけ返す
|
|
3624
|
+
return outerAbsPathInfo;
|
|
3625
|
+
}
|
|
3626
|
+
innerMapping.set(innerAbsPathInfo, outerAbsPathInfo);
|
|
3627
|
+
outerMapping.set(outerAbsPathInfo, innerAbsPathInfo);
|
|
3628
|
+
// ルールに対応するバインディングを生成し、親スコープの購読者として登録する。
|
|
3629
|
+
//
|
|
3630
|
+
// 子が読んだサブパス(inner "user.name" = outer "person.name")は、子が
|
|
3631
|
+
// そのパスに関心を宣言したということ。親がそこへ書いたときに子へ再読込通知が
|
|
3632
|
+
// 届くよう、プライマリと同じ形のバインディングを立てて絶対アドレス台帳に載せる。
|
|
3633
|
+
//
|
|
3634
|
+
// propSegments は stateProp(プライマリの先頭セグメント)を保つ必要がある。
|
|
3635
|
+
// 適用側は先頭セグメントで束ね先の state 要素を引く(apply/applyChangeToWebComponent.ts)
|
|
3636
|
+
// ため、inner パスだけにすると通知先を解決できない。
|
|
3637
|
+
//
|
|
3638
|
+
// 登録はプライマリを所有する BindingSession 経由で行う。台帳登録・teardown・
|
|
3639
|
+
// ノード削除時の破棄(MutationObserver 配送)が既存のライフサイクルにそのまま乗り、
|
|
3640
|
+
// 絶対アドレス台帳のエントリが component を強参照したまま残るのを防ぐ。
|
|
3641
|
+
// node 台帳(addBindingByNode)へは積まない — stateProp を保った結果、
|
|
3642
|
+
// 再バインド時に buildPrimaryMappingRule のプライマリ抽出フィルタへ混入するため。
|
|
3643
|
+
const propSegments = [primaryBinding.propSegments[0], ...innerAbsPathInfo.pathInfo.segments];
|
|
3644
|
+
const newBinding = {
|
|
3645
|
+
...primaryBinding,
|
|
3646
|
+
propName: propSegments.join(DELIMITER),
|
|
3647
|
+
propSegments,
|
|
3648
|
+
statePathName: outerAbsPathInfo.pathInfo.path,
|
|
3649
|
+
statePathInfo: outerAbsPathInfo.pathInfo,
|
|
3650
|
+
};
|
|
3651
|
+
// 登録できないケースは登録だけ諦める。ここは翻訳が本務なので read を落とさない
|
|
3652
|
+
// = この機構が入る前と同じ挙動に留める(debug 時のみ観測可能にする)。2 通りある。
|
|
3653
|
+
//
|
|
3654
|
+
// (a) セッションが引けない: 内部的な想定外(プライマリは親スコープの収集で必ず
|
|
3655
|
+
// session.initialize を通っている)。
|
|
3656
|
+
// (b) 導出した outer パスがワイルドカードを含むのに listIndex が決まらない:
|
|
3657
|
+
// 子が配列マッピングの上で for を回している場合(規則 state.items: rows に対し
|
|
3658
|
+
// 子の行が items.*.name を読む → outer は rows.*.name)。派生バインディングの
|
|
3659
|
+
// node は親スコープにあるコンポーネント要素で、ループは子の Shadow 内なので
|
|
3660
|
+
// コンポーネントからは行を特定できない = この 1 本では行を表現できない。
|
|
3661
|
+
// ここで登録を試みると getAbsoluteStateAddressByBinding が raiseError する。
|
|
3662
|
+
// この形の親→子配送は派生バインディングではなく、子の行バインディング自身を
|
|
3663
|
+
// 親のパターン台帳((absolutePathInfo, listIndex))へ相乗りさせて成立させる
|
|
3664
|
+
// (BindingSession.registerAddress / webComponent/outerListPath.ts、§1.8)。
|
|
3665
|
+
const skipRegistration = (reason) => {
|
|
3666
|
+
if (config.debug) {
|
|
3667
|
+
console.warn(`parent→child notification for "${outerAbsPathInfo.pathInfo.path}" is not registered: ${reason}.`, { webComponent, primaryBinding });
|
|
3668
|
+
}
|
|
3669
|
+
};
|
|
3670
|
+
const session = getBindingSession(primaryBinding);
|
|
3671
|
+
if (session === null) {
|
|
3672
|
+
skipRegistration('no binding session for the primary mapping rule');
|
|
3673
|
+
return outerAbsPathInfo;
|
|
3674
|
+
}
|
|
3675
|
+
if (outerAbsPathInfo.pathInfo.wildcardCount > 0 && getListIndexByBindingInfo(newBinding) === null) {
|
|
3676
|
+
skipRegistration('the derived outer path is a wildcard path but no list index resolves from the component');
|
|
3677
|
+
return outerAbsPathInfo;
|
|
3678
|
+
}
|
|
3679
|
+
// 戻り値(初期 apply 対象)は使わない。この導出は子の read の最中に起きるので、
|
|
3680
|
+
// 子は既に最新値を読んでおり、ここでの再通知は冗長かつ再入になる。
|
|
3681
|
+
session.initialize([newBinding], { registerAddress: true });
|
|
3682
|
+
return outerAbsPathInfo;
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
/**
|
|
3686
|
+
* webComponent/outerListPath.ts
|
|
3687
|
+
*
|
|
3688
|
+
* mapped な `bind-component` の子スコープが宣言した「リスト」を、値の正本を持つ
|
|
3689
|
+
* 親スコープ側へ伝えるための翻訳ヘルパ
|
|
3690
|
+
* (docs/architecture-hardening/15-state-component-mechanism-consistency.md §1.8)。
|
|
3691
|
+
*
|
|
3692
|
+
* 子の `for: items` が登録するのは子の state 要素の listPaths / elementPaths だけで、
|
|
3693
|
+
* 配列の実体を持つ親 state 要素は `rows` がリストであることを知らないままだった。
|
|
3694
|
+
* 親が `rows` を書いたときの依存 walk は `rows → rows.*` の静的子展開を
|
|
3695
|
+
* listPaths で判定するため、未登録だと行ごとの listIndex に展開されず
|
|
3696
|
+
* 「listIndex null のワイルドカードアドレス」1 本に潰れる(誰にも届かない)。
|
|
3697
|
+
* `rows.*` が elementPaths に無いと、行そのものへの代入(swap イディオム)も
|
|
3698
|
+
* listIndex 台帳の付け替えを伴わない素の代入に落ちる。
|
|
3699
|
+
*/
|
|
3700
|
+
/**
|
|
3701
|
+
* 子スコープの `for:` パスに対応する親スコープのパスへ「これはリストだ」を伝える。
|
|
3702
|
+
* マッピング規則が無い(plain なコンポーネント / ローカル state のリスト)場合は何もしない。
|
|
3703
|
+
*
|
|
3704
|
+
* 親がさらに別コンポーネントの mapped state であれば、その親の `setPathInfo` から
|
|
3705
|
+
* 再びここへ入って外向きに伝播する。各段で必ず外側の state 要素へ進むので停止する。
|
|
3706
|
+
*/
|
|
3707
|
+
function propagateListPathToOuterState(innerStateElement, innerPath) {
|
|
3708
|
+
const outerAbsPathInfo = resolveOuterAbsolutePathInfo(innerStateElement, getPathInfo(innerPath));
|
|
3709
|
+
if (outerAbsPathInfo === null || outerAbsPathInfo.stateElement === innerStateElement) {
|
|
3710
|
+
return;
|
|
3711
|
+
}
|
|
3712
|
+
outerAbsPathInfo.stateElement.setPathInfo(outerAbsPathInfo.pathInfo.path, "for");
|
|
3713
|
+
}
|
|
3714
|
+
/**
|
|
3715
|
+
* 子スコープのリスト行パス(`items.*.name`)に対応する親スコープの絶対パス情報を返す。
|
|
3716
|
+
*
|
|
3717
|
+
* 呼び出し側(`BindingSession.registerAddress`)は、行バインディングを**この外側パスと
|
|
3718
|
+
* 子スコープの listIndex の組**で親のパターン台帳に相乗りさせる。したがって成立条件は
|
|
3719
|
+
* 「子の listIndex が外側パスの段数をちょうど満たすこと」=
|
|
3720
|
+
* `Δ + innerW === outerW`(Δ = base 深さ)。
|
|
3721
|
+
*
|
|
3722
|
+
* - コンポーネントが親の `for` の外(Δ=0): `outerW === innerW`(§1.8)
|
|
3723
|
+
* - コンポーネントが親の `for` の中(Δ>0): 子の listIndex は base を親に持つので
|
|
3724
|
+
* チェーン長が Δ+innerW になり、そのまま外側パスの段数と一致する
|
|
3725
|
+
* (docs/state-bind-component-nested-for-design.md)
|
|
3726
|
+
*/
|
|
3727
|
+
function getOuterRowPathInfo(innerStateElement, innerPathInfo) {
|
|
3728
|
+
if (innerPathInfo.wildcardCount === 0) {
|
|
3729
|
+
return null;
|
|
3730
|
+
}
|
|
3731
|
+
const outerAbsPathInfo = resolveOuterAbsolutePathInfo(innerStateElement, innerPathInfo);
|
|
3732
|
+
if (outerAbsPathInfo === null || outerAbsPathInfo.stateElement === innerStateElement) {
|
|
3733
|
+
return null;
|
|
3734
|
+
}
|
|
3735
|
+
const baseDepth = getBaseDepth(innerStateElement);
|
|
3736
|
+
if (outerAbsPathInfo.pathInfo.wildcardCount !== innerPathInfo.wildcardCount + baseDepth) {
|
|
3737
|
+
return null;
|
|
3738
|
+
}
|
|
3739
|
+
return outerAbsPathInfo;
|
|
3740
|
+
}
|
|
3741
|
+
function resolveOuterAbsolutePathInfo(innerStateElement, innerPathInfo) {
|
|
3742
|
+
if (innerStateElement.hasMappedComponentState !== true) {
|
|
3743
|
+
return null;
|
|
3744
|
+
}
|
|
3745
|
+
const component = innerStateElement.boundComponent;
|
|
3746
|
+
if (component == null) {
|
|
3747
|
+
return null;
|
|
3748
|
+
}
|
|
3749
|
+
const innerAbsPathInfo = getAbsolutePathInfo(innerStateElement, innerPathInfo);
|
|
3750
|
+
// 参照専用で引く。ここはバインディング登録の最中(registerAddress → setPathInfo /
|
|
3751
|
+
// 行の相乗り登録)から呼ばれるので、翻訳のついでに購読者登録まで走らせると
|
|
3752
|
+
// `session.initialize` がセッション操作の内側から再入する。
|
|
3753
|
+
return getOuterAbsolutePathInfo(component, innerAbsPathInfo, false);
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3359
3756
|
// framework 自身が detach し明示的に解体(deactivate/unmount)したノード。
|
|
3360
3757
|
// BindingOwner の MutationObserver は削除サブツリー走査でこれらをスキップする。
|
|
3361
3758
|
//
|
|
@@ -4154,6 +4551,7 @@ class BindingSession {
|
|
|
4154
4551
|
address: null,
|
|
4155
4552
|
patternPathInfo: null,
|
|
4156
4553
|
patternListIndex: null,
|
|
4554
|
+
outerPatternPathInfo: null,
|
|
4157
4555
|
pendingDefinitions: 0,
|
|
4158
4556
|
initialPolicy: slot.policy,
|
|
4159
4557
|
resolvedAuthority: slot.authority,
|
|
@@ -4269,6 +4667,7 @@ class BindingSession {
|
|
|
4269
4667
|
address: null,
|
|
4270
4668
|
patternPathInfo: null,
|
|
4271
4669
|
patternListIndex: null,
|
|
4670
|
+
outerPatternPathInfo: null,
|
|
4272
4671
|
pendingDefinitions: 0,
|
|
4273
4672
|
initialPolicy: null,
|
|
4274
4673
|
resolvedAuthority: null,
|
|
@@ -4477,6 +4876,15 @@ class BindingSession {
|
|
|
4477
4876
|
addBindingByPattern(absolutePathInfo, listIndex, binding);
|
|
4478
4877
|
record.patternPathInfo = absolutePathInfo;
|
|
4479
4878
|
record.patternListIndex = listIndex;
|
|
4879
|
+
// mapped な bind-component の子スコープが回している行は、値の正本が親 state に
|
|
4880
|
+
// ある。親が行へ書いたときの enqueue は親の絶対パス情報で起きるので、同じ
|
|
4881
|
+
// listIndex(親子で共有されている)で親側のパターン台帳にも購読者として載せる。
|
|
4882
|
+
// これが無いと親起点の行フィールド書き込みが子に一切届かない(§1.8)。
|
|
4883
|
+
const outerPathInfo = getOuterRowPathInfo(stateElement, binding.statePathInfo);
|
|
4884
|
+
if (outerPathInfo !== null) {
|
|
4885
|
+
addBindingByPattern(outerPathInfo, listIndex, binding);
|
|
4886
|
+
record.outerPatternPathInfo = outerPathInfo;
|
|
4887
|
+
}
|
|
4480
4888
|
}
|
|
4481
4889
|
else {
|
|
4482
4890
|
const address = getAbsoluteStateAddressByBinding(binding, knownRoot);
|
|
@@ -4533,6 +4941,16 @@ class BindingSession {
|
|
|
4533
4941
|
}
|
|
4534
4942
|
}
|
|
4535
4943
|
else if (record.patternListIndex !== null) {
|
|
4944
|
+
// 親スコープへの相乗り分は独立した資源なので、子側の解除が失敗しても取り残さない
|
|
4945
|
+
if (record.outerPatternPathInfo !== null) {
|
|
4946
|
+
try {
|
|
4947
|
+
removeBindingByPattern(record.outerPatternPathInfo, record.patternListIndex, binding);
|
|
4948
|
+
}
|
|
4949
|
+
catch {
|
|
4950
|
+
// Cleanup is best-effort.
|
|
4951
|
+
}
|
|
4952
|
+
record.outerPatternPathInfo = null;
|
|
4953
|
+
}
|
|
4536
4954
|
try {
|
|
4537
4955
|
removeBindingByPattern(record.patternPathInfo, record.patternListIndex, binding);
|
|
4538
4956
|
record.patternPathInfo = null;
|
|
@@ -4590,21 +5008,35 @@ function getBindingSession(binding) {
|
|
|
4590
5008
|
return recordByBinding.get(binding)?.session ?? null;
|
|
4591
5009
|
}
|
|
4592
5010
|
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
5011
|
+
/**
|
|
5012
|
+
* `bind-component` の配線が完了した (webComponent, stateProp) の台帳。
|
|
5013
|
+
*
|
|
5014
|
+
* 完了前は state プロパティがまだ素のオブジェクトなので、親からの適用は
|
|
5015
|
+
* `applyChangeToProperty` がそこへ値を積み、`bindWebComponent` が melt して取り込む。
|
|
5016
|
+
* 完了後は公開プロパティが outerState proxy に差し替わっているため、親からの適用は
|
|
5017
|
+
* 値を運ばない内部通知チャネル(`applyChangeToWebComponent`)へ切り替わる。
|
|
5018
|
+
* その切り替え判定がこの台帳。
|
|
5019
|
+
*
|
|
5020
|
+
* キーは「state プロパティ名」であって state 要素ではない。完了はプロパティ単位の
|
|
5021
|
+
* 事実(`defineProperty(component, stateProp, ...)` が済んだか)であり、
|
|
5022
|
+
* 1 つの要素に複数の state プロパティを束ねられる以上、粒度もプロパティ単位が正しい。
|
|
5023
|
+
* 以前は内側の `IStateElement` をキーにしていたが、照会側(apply/applyChange.ts)が
|
|
5024
|
+
* 手にしているのは *親スコープ* の `IStateElement` であり、どちらも同じ型なので
|
|
5025
|
+
* TypeScript が取り違えを検出できず、判定が恒久的に false になっていた
|
|
5026
|
+
* (=親 state 起点の変更が子コンポーネントへ届かない。
|
|
5027
|
+
* docs/architecture-hardening/15-state-component-mechanism-consistency.md §1.7)。
|
|
5028
|
+
*/
|
|
5029
|
+
const completedStatePropsByWebComponent = new WeakMap();
|
|
5030
|
+
function markWebComponentAsComplete(webComponent, stateProp) {
|
|
5031
|
+
let completedStateProps = completedStatePropsByWebComponent.get(webComponent);
|
|
5032
|
+
if (!completedStateProps) {
|
|
5033
|
+
completedStateProps = new Set();
|
|
5034
|
+
completedStatePropsByWebComponent.set(webComponent, completedStateProps);
|
|
4599
5035
|
}
|
|
4600
|
-
|
|
5036
|
+
completedStateProps.add(stateProp);
|
|
4601
5037
|
}
|
|
4602
|
-
function isWebComponentComplete(webComponent,
|
|
4603
|
-
|
|
4604
|
-
if (!completeByStateElement) {
|
|
4605
|
-
return false;
|
|
4606
|
-
}
|
|
4607
|
-
return completeByStateElement.get(stateElement) === true;
|
|
5038
|
+
function isWebComponentComplete(webComponent, stateProp) {
|
|
5039
|
+
return completedStatePropsByWebComponent.get(webComponent)?.has(stateProp) === true;
|
|
4608
5040
|
}
|
|
4609
5041
|
|
|
4610
5042
|
function applyChangeToAttribute(binding, _context, newValue) {
|
|
@@ -5783,7 +6215,8 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
5783
6215
|
const listIndex = getListIndexByBindingInfo(bindingInfo);
|
|
5784
6216
|
const absAddress = getAbsoluteStateAddressByBinding(bindingInfo);
|
|
5785
6217
|
const lastValue = getLastListValueByAbsoluteStateAddress(absAddress);
|
|
5786
|
-
|
|
6218
|
+
// 子スコープのトップレベルのリストは base を親に持つ(webComponent/baseListIndex.ts)
|
|
6219
|
+
const diff = createListDiff(getListParentListIndex(context.stateElement, listIndex), lastValue, newValue);
|
|
5787
6220
|
context.newListValueByAbsAddress.set(absAddress, Array.isArray(newValue) ? newValue : []);
|
|
5788
6221
|
const fullDelete = Array.isArray(lastValue)
|
|
5789
6222
|
&& lastValue.length === diff.deleteIndexSet.size
|
|
@@ -5916,6 +6349,10 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
5916
6349
|
if (content === null) {
|
|
5917
6350
|
raiseError(`Content not found for ListIndex: ${index.index} at path "${listPathInfo.path}"`);
|
|
5918
6351
|
}
|
|
6352
|
+
// 祖先の unmount(if の非表示など)で解体された行は、ここで物理的に
|
|
6353
|
+
// 戻されるだけでは binding が dispose 済みのまま復活しない。位置合わせの
|
|
6354
|
+
// 前に判定しておき(mountAfter が mounted を立てる)、戻した後に再活性化する。
|
|
6355
|
+
const unmountedByAncestor = !content.mounted;
|
|
5919
6356
|
// Stable contents are already in correct relative order — but only
|
|
5920
6357
|
// trust that after physical verification (see isPhysicallyAfter).
|
|
5921
6358
|
// Contents out of order (and everything unverifiable) settle via the
|
|
@@ -5925,6 +6362,16 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
5925
6362
|
if (!stable && lastNode.nextSibling !== content.firstNode) {
|
|
5926
6363
|
content.mountAfter(lastNode);
|
|
5927
6364
|
}
|
|
6365
|
+
if (unmountedByAncestor) {
|
|
6366
|
+
// 再活性化しないと、行の同一性が保たれる更新が以後すべて無視される
|
|
6367
|
+
// (docs/state-deactivated-content-stale-update.md)。activate は
|
|
6368
|
+
// disposed record の再構築を含むので、プール再利用と同じ経路で戻る。
|
|
6369
|
+
const revivedContent = content;
|
|
6370
|
+
const stateAddress = createStateAddress(elementPathInfo, index);
|
|
6371
|
+
loopContextStack.createLoopContext(stateAddress, (loopContext) => {
|
|
6372
|
+
activateContent(revivedContent, loopContext, context);
|
|
6373
|
+
});
|
|
6374
|
+
}
|
|
5928
6375
|
}
|
|
5929
6376
|
lastNode = content.lastNode || lastNode;
|
|
5930
6377
|
if (typeof contentMap === 'undefined') {
|
|
@@ -6304,18 +6751,58 @@ function applyChangeToText(binding, _context, newValue) {
|
|
|
6304
6751
|
}
|
|
6305
6752
|
}
|
|
6306
6753
|
|
|
6307
|
-
|
|
6754
|
+
/**
|
|
6755
|
+
* 親 state → `bind-component` 済みコンポーネントの再読込通知(内部チャネル)。
|
|
6756
|
+
*
|
|
6757
|
+
* 値そのものは運ばない。バインドされたパスの正本は親 state 側にあり、子は
|
|
6758
|
+
* innerState proxy のマッピング経由で親を読みに行くため、必要なのは
|
|
6759
|
+
* 「そのパスを読み直せ」という通知だけ。
|
|
6760
|
+
*
|
|
6761
|
+
* 以前は `element[stateProp][path] = value` と、コンポーネントの公開プロパティを
|
|
6762
|
+
* 経由してこの通知を送っていた。受け側の proxy が値を捨てて `$postUpdate` を呼ぶ
|
|
6763
|
+
* 作りだったのはそのためだが、同じ proxy が `this.state` として作者にも見えていたので、
|
|
6764
|
+
* 公開 API 側の書き込みまで no-op になっていた。通知はここで state element を直接
|
|
6765
|
+
* 引く形に分離し、公開 proxy は素通し意味論に統一した
|
|
6766
|
+
* (docs/architecture-hardening/15-state-component-mechanism-consistency.md §1.1 / G1)。
|
|
6767
|
+
*
|
|
6768
|
+
* この関数が選ばれるのは `isWebComponentComplete` が真のときだけなので
|
|
6769
|
+
* (apply/applyChange.ts)、`bindWebComponent` は完了済み = state element は登録済み。
|
|
6770
|
+
* ただし**登録済みと使用可能は別**で、切断済みの state element が台帳に残っている
|
|
6771
|
+
* 窓がある(§1.9)。下の使用可能判定を参照。
|
|
6772
|
+
*/
|
|
6773
|
+
function applyChangeToWebComponent(binding, _context, _newValue) {
|
|
6308
6774
|
const element = binding.node;
|
|
6309
6775
|
const propSegments = binding.propSegments;
|
|
6310
6776
|
if (propSegments.length <= 1) {
|
|
6311
|
-
raiseError(`Invalid propSegments for web component binding: ${propSegments.join(
|
|
6777
|
+
raiseError(`Invalid propSegments for web component binding: ${propSegments.join(DELIMITER)}`);
|
|
6312
6778
|
}
|
|
6313
6779
|
const [firstSegment, ...restSegments] = propSegments;
|
|
6314
|
-
const
|
|
6315
|
-
if (
|
|
6316
|
-
raiseError(`
|
|
6780
|
+
const innerStateElement = getStateElementByWebComponent(element, firstSegment);
|
|
6781
|
+
if (innerStateElement === null) {
|
|
6782
|
+
raiseError(`State element not bound to "${firstSegment}" on web component.`);
|
|
6783
|
+
}
|
|
6784
|
+
// 切断済みの state element には送らない。
|
|
6785
|
+
//
|
|
6786
|
+
// リスト行にコンポーネントがあるとき、行の再生成では **DOM に戻る前に** apply が走る。
|
|
6787
|
+
// 行の content(と中のコンポーネント要素)は再利用されるので、要素をキーにした
|
|
6788
|
+
// 台帳 `stateElementByWebComponent` は前回の state element を指したままで、
|
|
6789
|
+
// その要素は既に切断されている(`rootNode` を失っている)。そこへ `createState` すると
|
|
6790
|
+
// raiseError し、**updater の drain も applyChangeToFor の行ループも例外を捕まえない**ため、
|
|
6791
|
+
// 1 つの行が同じバッチの残り全部を道連れにする — 実測では for が空になったまま、
|
|
6792
|
+
// 以後どんな更新でも復帰しなくなる(§1.9)。
|
|
6793
|
+
//
|
|
6794
|
+
// ここは値を運ばない再読込通知なので、切断中の子に送る意味がそもそも無い。
|
|
6795
|
+
// 子が DOM に戻れば、子のバインディングが innerState 経由で親をライブ読みするため
|
|
6796
|
+
// 現在値はそのとき正しく入る(初期配送と同じ経路)。よって no-op で落として良い。
|
|
6797
|
+
if (innerStateElement.hasRootNode === false) {
|
|
6798
|
+
if (config.debug) {
|
|
6799
|
+
console.debug(`[@wcstack/state] skipped parent→child notification for a disconnected state element on <${element.tagName.toLowerCase()}>.`, { element, stateProp: firstSegment, path: restSegments.join(DELIMITER) });
|
|
6800
|
+
}
|
|
6801
|
+
return;
|
|
6317
6802
|
}
|
|
6318
|
-
|
|
6803
|
+
innerStateElement.createState("readonly", (state) => {
|
|
6804
|
+
state.$postUpdate(restSegments.join(DELIMITER));
|
|
6805
|
+
});
|
|
6319
6806
|
}
|
|
6320
6807
|
|
|
6321
6808
|
// indexName ... $1, $2, ...
|
|
@@ -6327,7 +6814,7 @@ function getIndexValueByLoopContext(loopContext, indexName) {
|
|
|
6327
6814
|
if (typeof indexPos === "undefined") {
|
|
6328
6815
|
raiseError(`Invalid index name: ${indexName}`);
|
|
6329
6816
|
}
|
|
6330
|
-
const listIndex = loopContext.listIndex.
|
|
6817
|
+
const listIndex = listIndexAtWildcard(loopContext.listIndex, indexPos, loopContext.pathInfo.wildcardCount);
|
|
6331
6818
|
if (listIndex === null) {
|
|
6332
6819
|
raiseError(`Index not found at position ${indexPos} for loopContext:`);
|
|
6333
6820
|
}
|
|
@@ -6417,6 +6904,23 @@ const deferredSelectBindingByBinding = new WeakMap();
|
|
|
6417
6904
|
// 一度確認したら以後は不変(define は不可逆)なので apply 毎の getCustomElement /
|
|
6418
6905
|
// registry 照会を省略できる。scoped registry を導入する場合はこの不可逆前提を再検討。
|
|
6419
6906
|
const definedApplyVerifiedByBinding = new WeakMap();
|
|
6907
|
+
/**
|
|
6908
|
+
* このバインディングを「値を運ばない親→子の再読込通知」(applyChangeToWebComponent)へ
|
|
6909
|
+
* 回してよいか。
|
|
6910
|
+
*
|
|
6911
|
+
* 長さ 1 の propSegments を除くのが要点。`data-wcs="state: user"` のように
|
|
6912
|
+
* bind-component の stateProp をそのままプロパティ名に書いた形は、完了台帳のキーが
|
|
6913
|
+
* stateProp 名になった以上ゲートを通ってしまうが、applyChangeToWebComponent は
|
|
6914
|
+
* 「先頭セグメント=束ね先の state 要素、残り=子側のパス」を前提にしており
|
|
6915
|
+
* 残余が空だと raiseError する。updater の drain は例外を捕まえないので、
|
|
6916
|
+
* 誤設定タグ 1 つが同じバッチの無関係な更新まで巻き添えにしてしまう。
|
|
6917
|
+
* ここで弾いておけば従来どおり applyChangeToProperty に落ち、挙動は変わらない
|
|
6918
|
+
* (getter だけの公開プロパティへの代入が握り潰される = 無言の no-op)。
|
|
6919
|
+
*/
|
|
6920
|
+
function isWebComponentCompleteForBinding(binding) {
|
|
6921
|
+
return binding.propSegments.length > 1
|
|
6922
|
+
&& isWebComponentComplete(binding.replaceNode, binding.propSegments[0]);
|
|
6923
|
+
}
|
|
6420
6924
|
function _applyChange(binding, context) {
|
|
6421
6925
|
const value = getValue(context.state, binding);
|
|
6422
6926
|
const filteredValue = getFilteredValue(value, binding.outFilters);
|
|
@@ -6430,7 +6934,7 @@ function _applyChange(binding, context) {
|
|
|
6430
6934
|
return;
|
|
6431
6935
|
}
|
|
6432
6936
|
if (fnByBinding.has(binding)) {
|
|
6433
|
-
if (
|
|
6937
|
+
if (isWebComponentCompleteForBinding(binding)) {
|
|
6434
6938
|
fn = applyChangeToWebComponent;
|
|
6435
6939
|
fnByBinding.set(binding, fn); // 確定したのでキャッシュ
|
|
6436
6940
|
}
|
|
@@ -6448,7 +6952,7 @@ function _applyChange(binding, context) {
|
|
|
6448
6952
|
if (typeof fn === 'undefined') {
|
|
6449
6953
|
const customTag = getCustomElement(binding.replaceNode);
|
|
6450
6954
|
if (customTag) {
|
|
6451
|
-
if (
|
|
6955
|
+
if (isWebComponentCompleteForBinding(binding)) {
|
|
6452
6956
|
fn = applyChangeToWebComponent;
|
|
6453
6957
|
fnByBinding.set(binding, fn); // 確定したのでキャッシュ
|
|
6454
6958
|
}
|
|
@@ -7102,7 +7606,7 @@ async function buildBindings(root) {
|
|
|
7102
7606
|
}
|
|
7103
7607
|
}
|
|
7104
7608
|
|
|
7105
|
-
var version = "1.
|
|
7609
|
+
var version = "1.26.0";
|
|
7106
7610
|
var pkg = {
|
|
7107
7611
|
version: version};
|
|
7108
7612
|
|
|
@@ -7877,28 +8381,47 @@ function setStateElementByName(rootNode, name, element) {
|
|
|
7877
8381
|
// 初めてルートノードに登録する場合
|
|
7878
8382
|
// enable-ssr 属性があり、サーバーサイドでない場合はハイドレーション
|
|
7879
8383
|
const enableSsr = !inSsr() && element.hasAttribute?.('enable-ssr');
|
|
8384
|
+
// instanceof ではなく constructor.name で判定するのは意図的。SSR では
|
|
8385
|
+
// @wcstack/server の installGlobals が happy-dom の一部だけを globalThis に載せるが、
|
|
8386
|
+
// そのリスト(GLOBALS_KEYS)に `Document` は入っていない。Node にも `Document` は
|
|
8387
|
+
// 無いので `rootNode instanceof Document` は ReferenceError になる。
|
|
8388
|
+
// `ShadowRoot` はリストに含まれるため他所では instanceof を使っている
|
|
8389
|
+
// (docs/architecture-hardening/15-state-component-mechanism-consistency.md §3.3)。
|
|
8390
|
+
// reject を配管しないと、バインディング初期化中の例外は unhandled rejection として
|
|
8391
|
+
// 漏れるだけで ready が永久に未解決のまま残り、await getBindingsReady() の先が
|
|
8392
|
+
// 無言でハングする(docs/state-bind-component-nested-for-design.md §8.2)。
|
|
7880
8393
|
if (rootNode.constructor.name === 'HTMLDocument' || rootNode.constructor.name === 'Document') {
|
|
7881
|
-
const ready = new Promise((resolve) => {
|
|
8394
|
+
const ready = new Promise((resolve, reject) => {
|
|
7882
8395
|
queueMicrotask(async () => {
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
8396
|
+
try {
|
|
8397
|
+
if (enableSsr) {
|
|
8398
|
+
const success = await hydrateBindings(rootNode);
|
|
8399
|
+
if (!success) {
|
|
8400
|
+
await buildBindings(rootNode);
|
|
8401
|
+
}
|
|
8402
|
+
}
|
|
8403
|
+
else {
|
|
7886
8404
|
await buildBindings(rootNode);
|
|
7887
8405
|
}
|
|
8406
|
+
resolve();
|
|
7888
8407
|
}
|
|
7889
|
-
|
|
7890
|
-
|
|
8408
|
+
catch (error) {
|
|
8409
|
+
reject(error);
|
|
7891
8410
|
}
|
|
7892
|
-
resolve();
|
|
7893
8411
|
});
|
|
7894
8412
|
});
|
|
7895
8413
|
bindingsReadyByNode.set(rootNode, ready);
|
|
7896
8414
|
}
|
|
7897
8415
|
else if (rootNode.constructor.name === 'ShadowRoot') {
|
|
7898
|
-
const ready = new Promise((resolve) => {
|
|
8416
|
+
const ready = new Promise((resolve, reject) => {
|
|
7899
8417
|
queueMicrotask(async () => {
|
|
7900
|
-
|
|
7901
|
-
|
|
8418
|
+
try {
|
|
8419
|
+
await buildBindings(rootNode);
|
|
8420
|
+
resolve();
|
|
8421
|
+
}
|
|
8422
|
+
catch (error) {
|
|
8423
|
+
reject(error);
|
|
8424
|
+
}
|
|
7902
8425
|
});
|
|
7903
8426
|
});
|
|
7904
8427
|
bindingsReadyByNode.set(rootNode, ready);
|
|
@@ -8280,26 +8803,67 @@ function registerDevtoolsSource() {
|
|
|
8280
8803
|
getOrCreateHookRegistry().register(source);
|
|
8281
8804
|
}
|
|
8282
8805
|
|
|
8806
|
+
const CSP_GUIDE = "https://github.com/wcstack/wcstack/blob/main/docs/csp.md";
|
|
8807
|
+
/**
|
|
8808
|
+
* インライン `<script>` の評価失敗を、原因の分かるメッセージに変換する。
|
|
8809
|
+
*
|
|
8810
|
+
* CSP にブロックされた動的 import の rejection は
|
|
8811
|
+
* "Failed to fetch dynamically imported module" としか言わず、CSP には一切言及しない。
|
|
8812
|
+
* ブロックされた事実は securitypolicyviolation イベントでしか観測できないため、
|
|
8813
|
+
* その観測結果を `cspBlocked` で受け取る。
|
|
8814
|
+
*
|
|
8815
|
+
* 真ならブロック確定として対処方法まで書く。偽のときは構文エラー等と区別できないので、
|
|
8816
|
+
* 元のエラーを主にして CSP は参照先を添えるに留める(誤誘導を避ける)。
|
|
8817
|
+
*/
|
|
8818
|
+
function describeImportFailure(name, error, cspBlocked) {
|
|
8819
|
+
const detail = error?.message ?? String(error);
|
|
8820
|
+
if (cspBlocked) {
|
|
8821
|
+
return `The inline <script> of state "${name}" was blocked by Content-Security-Policy. ` +
|
|
8822
|
+
`Inline state is evaluated through a blob: URL, so script-src must allow blob:. ` +
|
|
8823
|
+
`Prefer moving the state into an external file and loading it with src="./state.js", ` +
|
|
8824
|
+
`which requires no extra CSP directive. See ${CSP_GUIDE}`;
|
|
8825
|
+
}
|
|
8826
|
+
return `Failed to evaluate the inline <script> of state "${name}": ${detail}. ` +
|
|
8827
|
+
`If this page sets a Content-Security-Policy, see ${CSP_GUIDE}`;
|
|
8828
|
+
}
|
|
8283
8829
|
async function loadFromInnerScript(script, name) {
|
|
8284
8830
|
let scriptModule = null;
|
|
8285
8831
|
const uniq_comment = `\n//# sourceURL=${name}\n`;
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
scriptModule = await import(url);
|
|
8832
|
+
// import() が失敗した理由が CSP かどうかを判別するために、評価の間だけ違反を購読する。
|
|
8833
|
+
let cspBlocked = false;
|
|
8834
|
+
const onViolation = (event) => {
|
|
8835
|
+
if (event.effectiveDirective.startsWith("script-src")) {
|
|
8836
|
+
cspBlocked = true;
|
|
8292
8837
|
}
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8838
|
+
};
|
|
8839
|
+
document.addEventListener("securitypolicyviolation", onViolation);
|
|
8840
|
+
try {
|
|
8841
|
+
if (typeof URL.createObjectURL === 'function') {
|
|
8842
|
+
// Create a blob URL for the script and dynamically import it
|
|
8843
|
+
const blob = new Blob([script.text + uniq_comment], { type: "application/javascript" });
|
|
8844
|
+
const url = URL.createObjectURL(blob);
|
|
8845
|
+
try {
|
|
8846
|
+
scriptModule = await import(url);
|
|
8847
|
+
}
|
|
8848
|
+
finally {
|
|
8849
|
+
// Clean up blob URL to prevent memory leak
|
|
8850
|
+
URL.revokeObjectURL(url);
|
|
8851
|
+
}
|
|
8852
|
+
}
|
|
8853
|
+
else {
|
|
8854
|
+
// Fallback: Base64 encoding method (for test environment)
|
|
8855
|
+
// Convert script to Base64 and import via data: URL
|
|
8856
|
+
const b64 = btoa(String.fromCodePoint(...new TextEncoder().encode(script.text + uniq_comment)));
|
|
8857
|
+
scriptModule = await import(`data:application/javascript;base64,${b64}`);
|
|
8296
8858
|
}
|
|
8297
8859
|
}
|
|
8298
|
-
|
|
8299
|
-
//
|
|
8300
|
-
//
|
|
8301
|
-
|
|
8302
|
-
|
|
8860
|
+
catch (e) {
|
|
8861
|
+
// 呼び出し元(State._initialize / _initializeDCC)が raiseError で
|
|
8862
|
+
// `[@wcstack/state]` を付けるため、ここでは prefix を重ねない。
|
|
8863
|
+
throw new Error(describeImportFailure(name, e, cspBlocked), { cause: e });
|
|
8864
|
+
}
|
|
8865
|
+
finally {
|
|
8866
|
+
document.removeEventListener("securitypolicyviolation", onViolation);
|
|
8303
8867
|
}
|
|
8304
8868
|
return (scriptModule && typeof scriptModule.default === 'object') ? scriptModule.default : {};
|
|
8305
8869
|
}
|
|
@@ -8346,6 +8910,10 @@ function loadFromScriptJson(id) {
|
|
|
8346
8910
|
class LoopContextStack {
|
|
8347
8911
|
_loopContextStack = Array(MAX_LOOP_DEPTH).fill(undefined);
|
|
8348
8912
|
_length = 0;
|
|
8913
|
+
_getBaseDepth;
|
|
8914
|
+
constructor(getBaseDepth) {
|
|
8915
|
+
this._getBaseDepth = getBaseDepth;
|
|
8916
|
+
}
|
|
8349
8917
|
createLoopContext(elementStateAddress, callback) {
|
|
8350
8918
|
if (elementStateAddress.listIndex === null) {
|
|
8351
8919
|
raiseError(`Cannot create loop context for a state address that does not have a list index.`);
|
|
@@ -8367,13 +8935,21 @@ class LoopContextStack {
|
|
|
8367
8935
|
}
|
|
8368
8936
|
else {
|
|
8369
8937
|
// With no active loop context the address must be self-contained: the
|
|
8370
|
-
// listIndex chain supplies one index per wildcard
|
|
8371
|
-
// (wildcardCount 1) always satisfy this. A nested
|
|
8372
|
-
// directly (e.g. replaced via $resolve from outside the
|
|
8373
|
-
// satisfies it — the for binding's listIndex carries the full
|
|
8374
|
-
// chain.
|
|
8938
|
+
// listIndex chain supplies one index per wildcard, plus this scope's base
|
|
8939
|
+
// depth Δ. Top-level lists (wildcardCount 1) always satisfy this. A nested
|
|
8940
|
+
// list re-rendered directly (e.g. replaced via $resolve from outside the
|
|
8941
|
+
// loop) also satisfies it — the for binding's listIndex carries the full
|
|
8942
|
+
// ancestor chain. Δ is non-zero only for a mapped bind-component child
|
|
8943
|
+
// whose host sits inside a parent-scope `for`
|
|
8944
|
+
// (docs/state-bind-component-nested-for-design.md).
|
|
8945
|
+
// ここは行ごとに通る(applyChangeToFor は追加行ごとに createLoopContext する)。
|
|
8946
|
+
// Δ=0 の判定を先に置き、通れば base 深さの解決(DOM の親走査を含む)に
|
|
8947
|
+
// 一切触れない — 通常の state に追加コストを載せないため。
|
|
8375
8948
|
if (loopContext.listIndex.length !== loopContext.pathInfo.wildcardCount) {
|
|
8376
|
-
|
|
8949
|
+
const baseDepth = this._getBaseDepth();
|
|
8950
|
+
if (loopContext.listIndex.length !== loopContext.pathInfo.wildcardCount + baseDepth) {
|
|
8951
|
+
raiseError(`Cannot push loop context when there is no active loop context: the list index chain (length ${loopContext.listIndex.length}) does not cover the wildcard path (wildcard count ${loopContext.pathInfo.wildcardCount}, base depth ${baseDepth}).`);
|
|
8952
|
+
}
|
|
8377
8953
|
}
|
|
8378
8954
|
}
|
|
8379
8955
|
this._loopContextStack[this._length] = loopContext;
|
|
@@ -8397,8 +8973,8 @@ class LoopContextStack {
|
|
|
8397
8973
|
return retValue;
|
|
8398
8974
|
}
|
|
8399
8975
|
}
|
|
8400
|
-
function createLoopContextStack() {
|
|
8401
|
-
return new LoopContextStack();
|
|
8976
|
+
function createLoopContextStack(getBaseDepth = () => 0) {
|
|
8977
|
+
return new LoopContextStack(getBaseDepth);
|
|
8402
8978
|
}
|
|
8403
8979
|
|
|
8404
8980
|
/**
|
|
@@ -8880,6 +9456,77 @@ function processStreamsDeclaration(stateElement, state) {
|
|
|
8880
9456
|
pruneLastNotified(stateElement, new Set(entries.keys()));
|
|
8881
9457
|
}
|
|
8882
9458
|
|
|
9459
|
+
/**
|
|
9460
|
+
* list/listKeys.ts
|
|
9461
|
+
*
|
|
9462
|
+
* `$listKeys: { <listPath>: <fieldName | (row) => key> }` 宣言マップを解析し、
|
|
9463
|
+
* 「リストパス → キー指定」表を構築する(docs/state-list-key-design.md §3)。
|
|
9464
|
+
*
|
|
9465
|
+
* この表が存在するリストパスへの配列代入は、setByAddress でキー突合され、
|
|
9466
|
+
* 一致行は旧オブジェクトを据え置いたまま変化フィールドだけが per-path 書き込みで
|
|
9467
|
+
* 流し込まれる(§2)。未宣言なら書き込み経路は従来と完全に同一。
|
|
9468
|
+
*
|
|
9469
|
+
* 「そのパスが実際にリストか」は宣言時には判定できない(listPaths は
|
|
9470
|
+
* バインディング収集時に確定する)。実行時に配列でなければ経路に入らないだけで、
|
|
9471
|
+
* 宣言自体はエラーにしない。
|
|
9472
|
+
*/
|
|
9473
|
+
/**
|
|
9474
|
+
* `$listKeys` 宣言を検証して Map 化する。宣言が無ければ null(=ゼロコスト経路)。
|
|
9475
|
+
*
|
|
9476
|
+
* 検証内容(§3.1):
|
|
9477
|
+
* - `$listKeys` はオブジェクト
|
|
9478
|
+
* - パスは非空文字列 / 空セグメント・先頭末尾の `.` を禁止
|
|
9479
|
+
* - パス末尾が `*` であることを禁止(リストパスであって要素パスではない)
|
|
9480
|
+
* - キー指定は非空文字列か関数
|
|
9481
|
+
* - 文字列キーは `.` / `*` を含まないフラットなフィールド名
|
|
9482
|
+
* - `Object.prototype` 継承名を禁止(`__proto__` / `constructor` 等)
|
|
9483
|
+
*/
|
|
9484
|
+
function processListKeysDeclaration(state) {
|
|
9485
|
+
const declared = state[STATE_LIST_KEYS_NAME];
|
|
9486
|
+
if (typeof declared === "undefined") {
|
|
9487
|
+
return null;
|
|
9488
|
+
}
|
|
9489
|
+
if (typeof declared !== "object" || declared === null) {
|
|
9490
|
+
raiseError(`${STATE_LIST_KEYS_NAME} must be an object mapping list paths to key specs.`);
|
|
9491
|
+
}
|
|
9492
|
+
const entries = new Map();
|
|
9493
|
+
for (const [path, spec] of Object.entries(declared)) {
|
|
9494
|
+
if (path.length === 0) {
|
|
9495
|
+
raiseError(`${STATE_LIST_KEYS_NAME} entry path must be a non-empty string.`);
|
|
9496
|
+
}
|
|
9497
|
+
const segments = path.split(DELIMITER);
|
|
9498
|
+
if (segments.some((segment) => segment.length === 0)) {
|
|
9499
|
+
raiseError(`${STATE_LIST_KEYS_NAME} entry "${path}" must not contain empty path segments.`);
|
|
9500
|
+
}
|
|
9501
|
+
if (segments[segments.length - 1] === WILDCARD) {
|
|
9502
|
+
raiseError(`${STATE_LIST_KEYS_NAME} entry "${path}" must be the list path itself, not the element path ` +
|
|
9503
|
+
`(drop the trailing "${DELIMITER}${WILDCARD}").`);
|
|
9504
|
+
}
|
|
9505
|
+
if (typeof spec === "function") {
|
|
9506
|
+
entries.set(path, spec);
|
|
9507
|
+
continue;
|
|
9508
|
+
}
|
|
9509
|
+
if (typeof spec !== "string") {
|
|
9510
|
+
raiseError(`${STATE_LIST_KEYS_NAME} entry "${path}" key spec must be a field name (string) or a function.`);
|
|
9511
|
+
}
|
|
9512
|
+
if (spec.length === 0) {
|
|
9513
|
+
raiseError(`${STATE_LIST_KEYS_NAME} entry "${path}" key field name must be a non-empty string.`);
|
|
9514
|
+
}
|
|
9515
|
+
if (spec.includes(DELIMITER) || spec.includes(WILDCARD)) {
|
|
9516
|
+
raiseError(`${STATE_LIST_KEYS_NAME} entry "${path}" key field name "${spec}" must be a flat property name ` +
|
|
9517
|
+
`("${DELIMITER}" / "${WILDCARD}" are not allowed).`);
|
|
9518
|
+
}
|
|
9519
|
+
// own key でなくても `in` 判定が真になる継承名は、キー抽出が prototype 由来の
|
|
9520
|
+
// 値(constructor など)を拾って全行同一キー扱いになるため名前の防衛線で落とす。
|
|
9521
|
+
if (spec in Object.prototype) {
|
|
9522
|
+
raiseError(`${STATE_LIST_KEYS_NAME} entry "${path}" key field name "${spec}" must not be a property name ` +
|
|
9523
|
+
`inherited from Object.prototype (e.g. "__proto__", "constructor").`);
|
|
9524
|
+
}
|
|
9525
|
+
entries.set(path, spec);
|
|
9526
|
+
}
|
|
9527
|
+
return entries.size > 0 ? entries : null;
|
|
9528
|
+
}
|
|
9529
|
+
|
|
8883
9530
|
/**
|
|
8884
9531
|
* stream/streamNamespace.ts
|
|
8885
9532
|
*
|
|
@@ -9429,6 +10076,12 @@ function getterFn(name) {
|
|
|
9429
10076
|
const stateEl = this.stateElement;
|
|
9430
10077
|
if (!stateEl)
|
|
9431
10078
|
return undefined;
|
|
10079
|
+
// state のロード前は「まだ値が無い」だけで異常ではない。行がまだ fragment 上にある間の
|
|
10080
|
+
// 初期スナップショット読み(BindingSession.readProducerSnapshot)は必ずここを通るので、
|
|
10081
|
+
// warn を出すと通常フローが騒がしくなる
|
|
10082
|
+
// (docs/architecture-hardening/15-state-component-mechanism-consistency.md §2.2)。
|
|
10083
|
+
if (stateEl.initialized !== true)
|
|
10084
|
+
return undefined;
|
|
9432
10085
|
let value;
|
|
9433
10086
|
try {
|
|
9434
10087
|
stateEl.createState("readonly", (state) => {
|
|
@@ -9447,6 +10100,15 @@ function setterFn(name) {
|
|
|
9447
10100
|
const stateEl = this.stateElement;
|
|
9448
10101
|
if (!stateEl)
|
|
9449
10102
|
return;
|
|
10103
|
+
// 初期化済みなら同期で書く。getter は同期なので、ここを常に initializePromise 経由に
|
|
10104
|
+
// すると `el.count = 5; el.count` が旧値を返す(§2.2)。未初期化のときだけ遅延させる
|
|
10105
|
+
// = 未接続の行に書かれた値が捨てられないための経路(§1.4)はそのまま残る。
|
|
10106
|
+
if (stateEl.initialized === true) {
|
|
10107
|
+
stateEl.createState("writable", (state) => {
|
|
10108
|
+
state[name] = value;
|
|
10109
|
+
});
|
|
10110
|
+
return;
|
|
10111
|
+
}
|
|
9450
10112
|
stateEl.initializePromise.then(() => {
|
|
9451
10113
|
stateEl.createState("writable", (state) => {
|
|
9452
10114
|
state[name] = value;
|
|
@@ -9455,6 +10117,8 @@ function setterFn(name) {
|
|
|
9455
10117
|
};
|
|
9456
10118
|
}
|
|
9457
10119
|
function callFn(name, isAsync) {
|
|
10120
|
+
// 戻り値は常に Promise。state 側のメソッドが同期でも初期化待ちが挟まりうるため、
|
|
10121
|
+
// 呼び出し側から見た型を揃える(wcBindable.commands が一律 `async: true` を宣言するのと対)。
|
|
9458
10122
|
if (isAsync) {
|
|
9459
10123
|
return function (...args) {
|
|
9460
10124
|
const stateEl = this.stateElement;
|
|
@@ -9485,10 +10149,116 @@ function isInternalProperty(name) {
|
|
|
9485
10149
|
return name.startsWith("$");
|
|
9486
10150
|
}
|
|
9487
10151
|
|
|
9488
|
-
function
|
|
10152
|
+
function getAllPropertyDescriptors(obj) {
|
|
10153
|
+
const chain = [];
|
|
10154
|
+
let proto = obj;
|
|
10155
|
+
while (proto && proto !== Object.prototype) {
|
|
10156
|
+
chain.push(proto);
|
|
10157
|
+
proto = Object.getPrototypeOf(proto);
|
|
10158
|
+
}
|
|
10159
|
+
const descriptors = {};
|
|
10160
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
10161
|
+
Object.assign(descriptors, Object.getOwnPropertyDescriptors(chain[i]));
|
|
10162
|
+
}
|
|
10163
|
+
return descriptors;
|
|
10164
|
+
}
|
|
10165
|
+
|
|
10166
|
+
/**
|
|
10167
|
+
* DCC の `$bindables` / `$commands` 宣言を解析・検証する。
|
|
10168
|
+
*
|
|
10169
|
+
* 検証の強度は `$commandTokens` / `$eventTokens` と揃える
|
|
10170
|
+
* (docs/architecture-hardening/15-state-component-mechanism-consistency.md §1.5 / §2.3 / §1.6)。
|
|
10171
|
+
* 従来 `$bindables` は `Array.isArray(...) ? ... : []` だけで、
|
|
10172
|
+
*
|
|
10173
|
+
* - 非配列を無言で空扱いにする
|
|
10174
|
+
* - 重複名をそのまま `createWcBindable` に流す
|
|
10175
|
+
* - `$` 始まりの名前を通す
|
|
10176
|
+
* - state に存在しない名前を通す
|
|
10177
|
+
*
|
|
10178
|
+
* という 4 つの穴があった。特に重複は害が大きい: `readNamedList`(protocol/wcBindableReader.ts)は
|
|
10179
|
+
* 重複名を見つけると `null` を返すため、`readBindableDeclaration()` が宣言全体を棄却し、
|
|
10180
|
+
* 双方向バインド・spread・initialSync の bindable 判定が**警告なしで**丸ごと死ぬ。
|
|
10181
|
+
* 自前のファクトリが自前の reader に棄却される状態なので、生成前に落とす。
|
|
10182
|
+
*/
|
|
10183
|
+
function readNameList(state, declarationName) {
|
|
10184
|
+
const declared = state[declarationName];
|
|
10185
|
+
if (typeof declared === "undefined") {
|
|
10186
|
+
return null;
|
|
10187
|
+
}
|
|
10188
|
+
if (!Array.isArray(declared)) {
|
|
10189
|
+
raiseError(`${declarationName} must be an array of strings.`);
|
|
10190
|
+
}
|
|
10191
|
+
const names = [];
|
|
10192
|
+
const seen = new Set();
|
|
10193
|
+
for (const name of declared) {
|
|
10194
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
10195
|
+
raiseError(`${declarationName} entries must be non-empty strings.`);
|
|
10196
|
+
}
|
|
10197
|
+
if (name.startsWith("$")) {
|
|
10198
|
+
raiseError(`${declarationName} entry "${name}" must not start with "$" (internal properties are not exposed on the component).`);
|
|
10199
|
+
}
|
|
10200
|
+
if (seen.has(name)) {
|
|
10201
|
+
raiseError(`${declarationName} entry "${name}" is duplicated.`);
|
|
10202
|
+
}
|
|
10203
|
+
seen.add(name);
|
|
10204
|
+
names.push(name);
|
|
10205
|
+
}
|
|
10206
|
+
return names;
|
|
10207
|
+
}
|
|
10208
|
+
/**
|
|
10209
|
+
* `$streams` が宣言している名前。値プロパティはインスタンス側の実体化まで state 上に
|
|
10210
|
+
* 現れないため、存在検査ではここも「実在する名前」として扱う(§2.3)。
|
|
10211
|
+
* 宣言そのものの妥当性検査は processStreamsDeclaration の責務なので、ここでは
|
|
10212
|
+
* キーの取り出しだけを行い、形が違えば黙って空集合を返す。
|
|
10213
|
+
*/
|
|
10214
|
+
function getStreamNames(state) {
|
|
10215
|
+
const declared = state[STATE_STREAMS_NAME];
|
|
10216
|
+
if (typeof declared !== "object" || declared === null) {
|
|
10217
|
+
return new Set();
|
|
10218
|
+
}
|
|
10219
|
+
return new Set(Object.keys(declared));
|
|
10220
|
+
}
|
|
10221
|
+
function processDccDeclarations(state) {
|
|
10222
|
+
const bindables = readNameList(state, STATE_BINDABLES_NAME) ?? [];
|
|
10223
|
+
const commands = readNameList(state, STATE_COMMANDS_NAME) ?? [];
|
|
10224
|
+
const descriptors = getAllPropertyDescriptors(state);
|
|
10225
|
+
const streamNames = getStreamNames(state);
|
|
10226
|
+
const streamBackedBindables = [];
|
|
10227
|
+
for (const name of bindables) {
|
|
10228
|
+
const descriptor = descriptors[name];
|
|
10229
|
+
if (typeof descriptor === "undefined") {
|
|
10230
|
+
// `$streams` 由来なら実体化後に現れるので通す。アクセサはこちらで補う。
|
|
10231
|
+
if (streamNames.has(name)) {
|
|
10232
|
+
streamBackedBindables.push(name);
|
|
10233
|
+
continue;
|
|
10234
|
+
}
|
|
10235
|
+
raiseError(`${STATE_BINDABLES_NAME} entry "${name}" is not declared on the state.`);
|
|
10236
|
+
}
|
|
10237
|
+
if (typeof descriptor.value === "function") {
|
|
10238
|
+
raiseError(`${STATE_BINDABLES_NAME} entry "${name}" is a method. Declare it in ${STATE_COMMANDS_NAME} instead.`);
|
|
10239
|
+
}
|
|
10240
|
+
}
|
|
10241
|
+
for (const name of commands) {
|
|
10242
|
+
const descriptor = descriptors[name];
|
|
10243
|
+
if (typeof descriptor === "undefined") {
|
|
10244
|
+
raiseError(`${STATE_COMMANDS_NAME} entry "${name}" is not declared on the state.`);
|
|
10245
|
+
}
|
|
10246
|
+
if (typeof descriptor.value !== "function") {
|
|
10247
|
+
raiseError(`${STATE_COMMANDS_NAME} entry "${name}" is not a method. Declare it in ${STATE_BINDABLES_NAME} instead.`);
|
|
10248
|
+
}
|
|
10249
|
+
}
|
|
10250
|
+
return { bindables, commands, streamBackedBindables };
|
|
10251
|
+
}
|
|
10252
|
+
|
|
10253
|
+
function createWcBindable(tagName, bindables, commands = []) {
|
|
9489
10254
|
const properties = bindables.map((propName) => ({
|
|
9490
10255
|
name: propName,
|
|
9491
10256
|
event: `${tagName}:${propName}-changed`,
|
|
10257
|
+
// Read the member off the element instead of trusting event.detail. The event is a
|
|
10258
|
+
// notification, not a carrier: a sub-path write (`user.name = "x"` against a `user`
|
|
10259
|
+
// member) has no single value to put in detail, and a state-side setter may normalize
|
|
10260
|
+
// what was written. Both cases are correct through the property.
|
|
10261
|
+
getter: (event) => event.target[propName],
|
|
9492
10262
|
}));
|
|
9493
10263
|
// Every $bindables member gets both a getter and a setter on the DCC prototype,
|
|
9494
10264
|
// so declare it in inputs as well — a property declared only in `properties` is
|
|
@@ -9497,12 +10267,24 @@ function createWcBindable(tagName, bindables) {
|
|
|
9497
10267
|
const inputs = bindables.map((propName) => ({
|
|
9498
10268
|
name: propName,
|
|
9499
10269
|
}));
|
|
9500
|
-
|
|
10270
|
+
const declaration = {
|
|
9501
10271
|
protocol: "wc-bindable",
|
|
9502
10272
|
version: 1,
|
|
9503
10273
|
properties,
|
|
9504
10274
|
inputs,
|
|
9505
10275
|
};
|
|
10276
|
+
if (commands.length === 0) {
|
|
10277
|
+
return declaration;
|
|
10278
|
+
}
|
|
10279
|
+
// `async: true` is uniform on purpose: dccPropertyFactories.callFn always chains on the
|
|
10280
|
+
// inner <wcs-state>'s initializePromise, so a DCC command returns a Promise whether or not
|
|
10281
|
+
// the underlying state method was declared `async`. Reporting the state method's own
|
|
10282
|
+
// asyncness would describe something callers never observe.
|
|
10283
|
+
const declaredCommands = commands.map((name) => ({
|
|
10284
|
+
name,
|
|
10285
|
+
async: true,
|
|
10286
|
+
}));
|
|
10287
|
+
return { ...declaration, commands: declaredCommands };
|
|
9506
10288
|
}
|
|
9507
10289
|
function createBindableEventMap(tagName, bindables) {
|
|
9508
10290
|
const map = {};
|
|
@@ -9519,20 +10301,20 @@ function defineDCC(hostElement, shadowRoot, state) {
|
|
|
9519
10301
|
raiseError(`DCC: "${tagName}" is not a valid custom element name (must contain a hyphen).`);
|
|
9520
10302
|
}
|
|
9521
10303
|
if (customElements.get(tagName)) {
|
|
9522
|
-
//
|
|
9523
|
-
|
|
9524
|
-
|
|
10304
|
+
// 重複定義は authoring error として落とす。従来は warn してスキップしていたが、
|
|
10305
|
+
// 先勝ちで別テンプレートのインスタンスが生えるため「動いているように見えて中身が違う」
|
|
10306
|
+
// 状態になる。state 名の重複(stateElementByName)が raiseError なのと作法を揃える
|
|
10307
|
+
// (docs/architecture-hardening/15-state-component-mechanism-consistency.md §3.4)。
|
|
10308
|
+
raiseError(`DCC: "${tagName}" is already registered. A custom element name can only be defined once.`);
|
|
9525
10309
|
}
|
|
9526
10310
|
// ShadowRoot は cloneNode 不可のため、template 経由で内容をクローン
|
|
9527
10311
|
const template = document.createElement("template");
|
|
9528
10312
|
template.innerHTML = shadowRoot.innerHTML;
|
|
9529
10313
|
const shadowRootMode = shadowRoot.mode;
|
|
9530
|
-
// $bindables から wcBindable + bindableEventMap を生成
|
|
9531
|
-
const bindables =
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
const wcBindable = bindables.length > 0
|
|
9535
|
-
? createWcBindable(tagName, bindables)
|
|
10314
|
+
// $bindables / $commands から wcBindable + bindableEventMap を生成
|
|
10315
|
+
const { bindables, commands, streamBackedBindables } = processDccDeclarations(state);
|
|
10316
|
+
const wcBindable = (bindables.length > 0 || commands.length > 0)
|
|
10317
|
+
? createWcBindable(tagName, bindables, commands)
|
|
9536
10318
|
: null;
|
|
9537
10319
|
const bindableEventMap = bindables.length > 0
|
|
9538
10320
|
? createBindableEventMap(tagName, bindables)
|
|
@@ -9545,27 +10327,79 @@ function defineDCC(hostElement, shadowRoot, state) {
|
|
|
9545
10327
|
static wcBindable = wcBindable;
|
|
9546
10328
|
static bindableEventMap = bindableEventMap;
|
|
9547
10329
|
_shadow = null;
|
|
9548
|
-
|
|
10330
|
+
/**
|
|
10331
|
+
* shadow を遅延構築する。定義要素(`data-wc-definition`)では null を返す。
|
|
10332
|
+
*
|
|
10333
|
+
* connectedCallback ではなくここで張るのは、**接続前にアクセサが呼ばれる**ため
|
|
10334
|
+
* (§1.4)。`for` の全追加パスは行を fragment に組み立ててからバインドを適用し、
|
|
10335
|
+
* fragment を DOM に挿すのは最後なので、`element.count = v` の時点で行はまだ未接続。
|
|
10336
|
+
* shadow が無いと `stateElement` が null になり、setterFn が無言で書き込みを捨てていた。
|
|
10337
|
+
* ここで構築しておけば、書き込みは inner `<wcs-state>` の initializePromise に
|
|
10338
|
+
* 積まれ、接続・state ロード後に適用される。
|
|
10339
|
+
*
|
|
10340
|
+
* 冪等なので再接続でも張り直さない。shadow tree は host の切断後も保持され、
|
|
10341
|
+
* 2 回目の attachShadow は NotSupportedError になる(§1.3)。`if` の false→true
|
|
10342
|
+
* 再マウントと `for` の行プーリングはどちらも同一ノードを unmount → mount する。
|
|
10343
|
+
* closed mode では `this.shadowRoot` が null なので判定はフィールド側で行う。
|
|
10344
|
+
*
|
|
10345
|
+
* G4 は「constructor へ前倒し」で決着したが、実装は constructor ではなく
|
|
10346
|
+
* この遅延構築を採った。目的(未接続でもアクセサが動く)は同じで、constructor 版だと
|
|
10347
|
+
* (1) 定義要素の判定に属性を読む必要があり constructor の作法に反する、
|
|
10348
|
+
* (2) 同一タグの `data-wc-definition` が 2 つある場合、DSD の shadow を既に持つ
|
|
10349
|
+
* 2 つ目に attachShadow して throw する、の 2 点を踏むため。
|
|
10350
|
+
*/
|
|
10351
|
+
_ensureShadow() {
|
|
10352
|
+
if (this._shadow !== null)
|
|
10353
|
+
return this._shadow;
|
|
9549
10354
|
if (this.hasAttribute(DCC_DEFINITION_ATTRIBUTE))
|
|
9550
|
-
return;
|
|
10355
|
+
return null;
|
|
9551
10356
|
this._shadow = this.attachShadow({ mode: DCCElement.shadowRootMode });
|
|
9552
10357
|
this._shadow.appendChild(DCCElement.template.content.cloneNode(true));
|
|
9553
|
-
//
|
|
10358
|
+
// template.content は inert なテンプレート所有ドキュメントに属するため、その clone は
|
|
10359
|
+
// カスタム要素として upgrade されていない。ホストが接続済みなら appendChild の時点で
|
|
10360
|
+
// upgrade されるが、未接続の shadow に挿した場合は upgrade 契機が無く、内側の
|
|
10361
|
+
// <wcs-state> が素の HTMLElement のまま残って createState が生えない。明示的に upgrade する。
|
|
10362
|
+
const registry = getCustomElementRegistry();
|
|
10363
|
+
if (registry !== null) {
|
|
10364
|
+
upgradeCustomElement(registry, this._shadow);
|
|
10365
|
+
}
|
|
10366
|
+
return this._shadow;
|
|
10367
|
+
}
|
|
10368
|
+
connectedCallback() {
|
|
10369
|
+
const shadow = this._ensureShadow();
|
|
10370
|
+
if (shadow === null)
|
|
10371
|
+
return;
|
|
10372
|
+
// bindableEventMap の設定。
|
|
10373
|
+
// initializePromise は待たない。待つと state のロード完了まで map が空のままで、
|
|
10374
|
+
// $connectedCallback 内で行った初期変更が変更イベントを出さない
|
|
10375
|
+
// (docs/architecture-hardening/15-state-component-mechanism-consistency.md §2.7)。
|
|
10376
|
+
// setBindableEventMap はフィールド代入だけで state を参照しないので、
|
|
10377
|
+
// <wcs-state> の初期化前に呼んでも安全。
|
|
9554
10378
|
if (Object.keys(DCCElement.bindableEventMap).length > 0) {
|
|
9555
|
-
const stateEl =
|
|
10379
|
+
const stateEl = shadow.querySelector(stateTagSelector);
|
|
9556
10380
|
if (stateEl) {
|
|
9557
|
-
stateEl.
|
|
9558
|
-
|
|
9559
|
-
|
|
10381
|
+
stateEl.setBindableEventMap(DCCElement.bindableEventMap);
|
|
10382
|
+
}
|
|
10383
|
+
else {
|
|
10384
|
+
// $bindables を宣言しているのに束ねる先が無い。stateTagSelector は
|
|
10385
|
+
// `:not([name])` なので name 付きの <wcs-state> は一致せず、この分岐に落ちると
|
|
10386
|
+
// 変更イベントが一切出ないまま静かに壊れる
|
|
10387
|
+
// (docs/architecture-hardening/15-state-component-mechanism-consistency.md §2.5)。
|
|
10388
|
+
console.warn(`[@wcstack/state] DCC: "${tagName}" declares ${STATE_BINDABLES_NAME} but its template has no <${config.tagNames.state}> without a "name" attribute. Change events will not be dispatched.`);
|
|
9560
10389
|
}
|
|
9561
10390
|
}
|
|
9562
10391
|
}
|
|
9563
10392
|
get stateElement() {
|
|
9564
|
-
|
|
10393
|
+
// 未接続でも shadow を構築して解決する(§1.4)。
|
|
10394
|
+
return (this._ensureShadow()?.querySelector(stateTagSelector) ?? null);
|
|
9565
10395
|
}
|
|
9566
10396
|
};
|
|
9567
|
-
// state プロパティを走査して DCC クラスのプロトタイプにgetter/setter/method
|
|
9568
|
-
|
|
10397
|
+
// state プロパティを走査して DCC クラスのプロトタイプにgetter/setter/methodを定義。
|
|
10398
|
+
// 走査範囲は State の getterPaths / setterPaths 収集と同じ「自身+プロトタイプチェーン」に
|
|
10399
|
+
// 揃える。own descriptor だけを見ていた頃は、クラスインスタンスや Object.create(proto) の
|
|
10400
|
+
// state で「getterPaths には載るのにアクセサが生えない」乖離が出ていた
|
|
10401
|
+
// (docs/architecture-hardening/15-state-component-mechanism-consistency.md §2.4)。
|
|
10402
|
+
const descriptors = getAllPropertyDescriptors(state);
|
|
9569
10403
|
for (const [name, desc] of Object.entries(descriptors)) {
|
|
9570
10404
|
if (isInternalProperty(name))
|
|
9571
10405
|
continue;
|
|
@@ -9580,6 +10414,17 @@ function defineDCC(hostElement, shadowRoot, state) {
|
|
|
9580
10414
|
}
|
|
9581
10415
|
Object.defineProperty(DCCElement.prototype, name, newDesc);
|
|
9582
10416
|
}
|
|
10417
|
+
// `$streams` の値プロパティはインスタンス側の processStreamsDeclaration で実体化されるため、
|
|
10418
|
+
// defineDCC の時点では state 上に descriptor が無い。$bindables に載っているのに
|
|
10419
|
+
// アクセサが生えないと宣言だけが生きて要素側が expando を掴むので、ここで補う(§2.3)。
|
|
10420
|
+
for (const name of streamBackedBindables) {
|
|
10421
|
+
Object.defineProperty(DCCElement.prototype, name, {
|
|
10422
|
+
configurable: true,
|
|
10423
|
+
enumerable: true,
|
|
10424
|
+
get: getterFn(name),
|
|
10425
|
+
set: setterFn(name),
|
|
10426
|
+
});
|
|
10427
|
+
}
|
|
9583
10428
|
// カスタム要素登録
|
|
9584
10429
|
customElements.define(tagName, DCCElement);
|
|
9585
10430
|
}
|
|
@@ -9747,6 +10592,56 @@ function dirtyCacheEntryByAbsoluteStateAddress(address) {
|
|
|
9747
10592
|
}
|
|
9748
10593
|
}
|
|
9749
10594
|
|
|
10595
|
+
/**
|
|
10596
|
+
* webComponent/crossBoundaryAddress.ts
|
|
10597
|
+
*
|
|
10598
|
+
* mapped な `bind-component` の state は innerState proxy を target に持つ。
|
|
10599
|
+
* そこへの読み書きは `Reflect.get/set(target, path)` で行われるため、Proxy の
|
|
10600
|
+
* トラップに渡るのは**パス文字列だけ**で、解決済みの listIndex が落ちる。
|
|
10601
|
+
*
|
|
10602
|
+
* 子スコープが `for:` でマップ先の配列を回している場合、行バインディングが読む
|
|
10603
|
+
* `items.*.name` は親スコープの `rows.*.name` に翻訳されるが、どの行かは listIndex
|
|
10604
|
+
* にしか無い。ループ文脈はコンポーネント要素(親スコープ側)にぶら下がっており、
|
|
10605
|
+
* 子スコープのループはコンポーネントの内側なので `getLoopContextByNode` では引けない
|
|
10606
|
+
* (docs/architecture-hardening/15-state-component-mechanism-consistency.md §1.8)。
|
|
10607
|
+
*
|
|
10608
|
+
* そこで越境直前のアドレスを動的スコープで受け渡す。push/pop するのは
|
|
10609
|
+
* `hasMappedComponentState` が真の state 要素の読み書きだけで、通常の state の
|
|
10610
|
+
* ホットパス(getByAddress / setByAddress)には一切載らない。
|
|
10611
|
+
*/
|
|
10612
|
+
// 行ごとの読み書きで通るため、エントリオブジェクトを割り当てずに 2 本の並行配列で持つ
|
|
10613
|
+
// (リスト描画は「行数 × 行内バインディング数」回ここを通る)。
|
|
10614
|
+
const stateElementStack = [];
|
|
10615
|
+
const addressStack = [];
|
|
10616
|
+
let depth = 0;
|
|
10617
|
+
function pushCrossBoundaryAddress(stateElement, address) {
|
|
10618
|
+
stateElementStack[depth] = stateElement;
|
|
10619
|
+
addressStack[depth] = address;
|
|
10620
|
+
depth++;
|
|
10621
|
+
}
|
|
10622
|
+
function popCrossBoundaryAddress() {
|
|
10623
|
+
depth--;
|
|
10624
|
+
// 参照を残さない(state 要素・listIndex を保持し続けないため)
|
|
10625
|
+
stateElementStack[depth] = undefined;
|
|
10626
|
+
addressStack[depth] = undefined;
|
|
10627
|
+
}
|
|
10628
|
+
/**
|
|
10629
|
+
* 越境直前のアドレスを取り出す。スタック最上位が「この state 要素の、このパスの
|
|
10630
|
+
* 読み書き」であるときだけ返す。ネストしたコンポーネントでは最内の越境が
|
|
10631
|
+
* 最上位になるため、同一性の照合だけで取り違えを防げる。
|
|
10632
|
+
*/
|
|
10633
|
+
function getCrossBoundaryAddress(stateElement, path) {
|
|
10634
|
+
if (depth === 0) {
|
|
10635
|
+
return null;
|
|
10636
|
+
}
|
|
10637
|
+
const top = depth - 1;
|
|
10638
|
+
const address = addressStack[top];
|
|
10639
|
+
if (stateElementStack[top] !== stateElement || address?.pathInfo.path !== path) {
|
|
10640
|
+
return null;
|
|
10641
|
+
}
|
|
10642
|
+
return address;
|
|
10643
|
+
}
|
|
10644
|
+
|
|
9750
10645
|
function checkDependency(handler, address) {
|
|
9751
10646
|
// $untrackDependency スコープ中/setter 実行中は依存を張らない
|
|
9752
10647
|
if (handler.untracking) {
|
|
@@ -9770,9 +10665,18 @@ function checkDependency(handler, address) {
|
|
|
9770
10665
|
if (address.pathInfo.wildcardCount > 0 && lastInfo.wildcardCount > 0) {
|
|
9771
10666
|
const sharedLen = calcWildcardLen(address.pathInfo, lastInfo);
|
|
9772
10667
|
if (sharedLen > 0) {
|
|
10668
|
+
// 共有ワイルドカード段の突き合わせ。base 深さ Δ を持つ子スコープでは
|
|
10669
|
+
// 先頭起点だと Δ 段目(=親子で常に同一の base)を比べてしまい、
|
|
10670
|
+
// 本物の他行読み取りを取りこぼす。末尾起点で数える(list/wildcardLevel.ts)
|
|
9773
10671
|
let crossRow = false;
|
|
10672
|
+
const hereChain = address.listIndex ?? null;
|
|
10673
|
+
const thereChain = lastAddress.listIndex ?? null;
|
|
9774
10674
|
for (let level = 0; level < sharedLen; level++) {
|
|
9775
|
-
|
|
10675
|
+
const here = hereChain !== null
|
|
10676
|
+
? listIndexAtWildcard(hereChain, level, address.pathInfo.wildcardCount) : null;
|
|
10677
|
+
const there = thereChain !== null
|
|
10678
|
+
? listIndexAtWildcard(thereChain, level, lastInfo.wildcardCount) : null;
|
|
10679
|
+
if (here !== there) {
|
|
9776
10680
|
crossRow = true;
|
|
9777
10681
|
break;
|
|
9778
10682
|
}
|
|
@@ -9789,6 +10693,28 @@ function checkDependency(handler, address) {
|
|
|
9789
10693
|
}
|
|
9790
10694
|
}
|
|
9791
10695
|
|
|
10696
|
+
/**
|
|
10697
|
+
* このアドレスの値をキャッシュしてよいか(getByAddress / setByAddress 共通の判定)。
|
|
10698
|
+
*
|
|
10699
|
+
* ワイルドカードを含むパス(リスト行)と宣言済み getter は再評価が高くつくため
|
|
10700
|
+
* キャッシュする。ただし mapped な `bind-component` の state は例外で、丸ごと外す。
|
|
10701
|
+
*
|
|
10702
|
+
* mapped な state は値を持たず、読みも書きも親スコープの state へ解決される
|
|
10703
|
+
* (innerState proxy)。同じ値は親側のキャッシュにも載り、その無効化は親の依存 walk が
|
|
10704
|
+
* 担う。子側にもう一段キャッシュを置くと、正本でない複製を親の無効化が届かない場所に
|
|
10705
|
+
* 作ることになり、親起点の書き込みのあと子だけが旧値を読み続ける。二重に持たないのが
|
|
10706
|
+
* 唯一の整合手段なので、mapped な state 要素ではキャッシュ層を持たない — 親の
|
|
10707
|
+
* キャッシュがそのまま効くので、失うのは重複していた一段だけ
|
|
10708
|
+
* (docs/architecture-hardening/15-state-component-mechanism-consistency.md §1.8)。
|
|
10709
|
+
*/
|
|
10710
|
+
function isCacheable(stateElement, address) {
|
|
10711
|
+
if (stateElement.hasMappedComponentState === true) {
|
|
10712
|
+
return false;
|
|
10713
|
+
}
|
|
10714
|
+
return address.pathInfo.wildcardCount > 0 ||
|
|
10715
|
+
stateElement.getterPaths.has(address.pathInfo.path);
|
|
10716
|
+
}
|
|
10717
|
+
|
|
9792
10718
|
/**
|
|
9793
10719
|
* getByAddress.ts
|
|
9794
10720
|
*
|
|
@@ -9856,6 +10782,17 @@ function _getByAddress(target, address, receiver, handler, stateElement) {
|
|
|
9856
10782
|
handler.popAddress();
|
|
9857
10783
|
}
|
|
9858
10784
|
}
|
|
10785
|
+
else if (stateElement.hasMappedComponentState === true) {
|
|
10786
|
+
// target は innerState proxy。get トラップにはパス文字列しか渡らないので、
|
|
10787
|
+
// 解決済みの listIndex を動的スコープで越境させる(§1.8)
|
|
10788
|
+
pushCrossBoundaryAddress(stateElement, address);
|
|
10789
|
+
try {
|
|
10790
|
+
return Reflect.get(target, address.pathInfo.path);
|
|
10791
|
+
}
|
|
10792
|
+
finally {
|
|
10793
|
+
popCrossBoundaryAddress();
|
|
10794
|
+
}
|
|
10795
|
+
}
|
|
9859
10796
|
else {
|
|
9860
10797
|
return Reflect.get(target, address.pathInfo.path);
|
|
9861
10798
|
}
|
|
@@ -9863,6 +10800,20 @@ function _getByAddress(target, address, receiver, handler, stateElement) {
|
|
|
9863
10800
|
else {
|
|
9864
10801
|
const parentAddress = address.parentAddress ?? raiseError(`address.parentAddress is undefined path: ${address.pathInfo.path}`);
|
|
9865
10802
|
const parentValue = getByAddress(target, parentAddress, receiver, handler);
|
|
10803
|
+
// 親が居ないパスの読みは undefined(=「state に意見が無い」)。`Reflect.get` に
|
|
10804
|
+
// そのまま渡すと生の `TypeError: Reflect.get called on non-object` になり、
|
|
10805
|
+
// updater の drain も行ループも捕まえないので **1 本の stale な読みが同じバッチの
|
|
10806
|
+
// 無関係な更新まで道連れにする**(§1.7 / §1.9 と同じ構図)。
|
|
10807
|
+
//
|
|
10808
|
+
// 実際に踏むのは「消えた行を指すバインディングが、その行を消す `for` より先に
|
|
10809
|
+
// 適用される」形。同一スコープならトポロジカル順で `for` が先に来るので起きないが、
|
|
10810
|
+
// bind-component は親スコープの通知と子スコープの `for` が別経路で流れるため
|
|
10811
|
+
// 順序が保証されない(docs/state-bind-component-nested-for-design.md)。
|
|
10812
|
+
// undefined はプロパティ書き込みがスキップされる値なので DOM は触られず、
|
|
10813
|
+
// 直後に `for` が行ごと外して整合する。
|
|
10814
|
+
if (parentValue === null || typeof parentValue === "undefined") {
|
|
10815
|
+
return undefined;
|
|
10816
|
+
}
|
|
9866
10817
|
const lastSegment = address.pathInfo.segments[address.pathInfo.segments.length - 1];
|
|
9867
10818
|
if (lastSegment === WILDCARD) {
|
|
9868
10819
|
const index = address.listIndex?.index ?? raiseError(`address.listIndex?.index is undefined path: ${address.pathInfo.path}`);
|
|
@@ -9892,8 +10843,7 @@ function getByAddress(target, address, receiver, handler) {
|
|
|
9892
10843
|
// $streams の args トレース中のみ絶対アドレスを捕捉(collector 非活性なら即 return)
|
|
9893
10844
|
collectStreamDependency(handler.stateElement, address);
|
|
9894
10845
|
const stateElement = handler.stateElement;
|
|
9895
|
-
const cacheable = address
|
|
9896
|
-
stateElement.getterPaths.has(address.pathInfo.path);
|
|
10846
|
+
const cacheable = isCacheable(stateElement, address);
|
|
9897
10847
|
if (cacheable) {
|
|
9898
10848
|
return _getByAddressWithCache(target, address, receiver, handler, stateElement);
|
|
9899
10849
|
}
|
|
@@ -9931,7 +10881,181 @@ function getContextListIndex(handler, structuredPath) {
|
|
|
9931
10881
|
if (typeof index === "undefined") {
|
|
9932
10882
|
return null;
|
|
9933
10883
|
}
|
|
9934
|
-
|
|
10884
|
+
if (address.listIndex === null) {
|
|
10885
|
+
return null;
|
|
10886
|
+
}
|
|
10887
|
+
return listIndexAtWildcard(address.listIndex, index, address.pathInfo.wildcardCount);
|
|
10888
|
+
}
|
|
10889
|
+
|
|
10890
|
+
/**
|
|
10891
|
+
* DCC の `$bindables` メンバの変更イベントを host に dispatch する。
|
|
10892
|
+
*
|
|
10893
|
+
* 対応するのは 3 通り。
|
|
10894
|
+
*
|
|
10895
|
+
* 1. **完全一致** — `count = 1` が `count` メンバを撃つ。`detail` は書き込んだ値。
|
|
10896
|
+
* 2. **サブパス** — `user.name = "x"` や `items.0.done = true` が `user` / `items` メンバを撃つ。
|
|
10897
|
+
* `$bindables` のエントリは常にフラットなトップレベル名(dotted 名は
|
|
10898
|
+
* processDccDeclarations の存在検査で落ちる)なので、先頭セグメントを見れば足りる。
|
|
10899
|
+
* この場合 `detail` は付かない — メンバ全体ではない値を載せると誤解を招くため。
|
|
10900
|
+
* 3. **`$postUpdate`** — in-place 変異を通知する正規の idiom。書き込んだ値が無いので `detail` は付かない。
|
|
10901
|
+
*
|
|
10902
|
+
* `detail` に頼らないのが正しい読み方で、`createWcBindable` は各 property に
|
|
10903
|
+
* `getter: (event) => event.target[name]` を宣言している。observer はイベントを
|
|
10904
|
+
* 「変わった」という通知として受け取り、値は要素から読む。
|
|
10905
|
+
*
|
|
10906
|
+
* 従来は完全一致しか見ておらず、`$bindables: ["user"]` で `user.name` を書いても
|
|
10907
|
+
* 発火しなかった。wc-bindable の `properties[].event` は「変更で発火する」契約なので乖離していた
|
|
10908
|
+
* (docs/architecture-hardening/15-state-component-mechanism-consistency.md §2.1)。
|
|
10909
|
+
*
|
|
10910
|
+
* 配列の in-place 変異(`items.push(...)`)は set トラップを通らないため、ここでも捕まらない。
|
|
10911
|
+
* これはリアクティブコア全体の規範(in-place 変異は `$postUpdate` で通知する)と同じで、
|
|
10912
|
+
* 正しい idiom を踏めば 3 で発火する。
|
|
10913
|
+
*
|
|
10914
|
+
* `$listKeys` を宣言したリストは、配列代入がキー突合後に per-path 書き込みへ分解されるため
|
|
10915
|
+
* (docs/state-list-key-design.md §2)、1 回の代入で `1 + 変化行数` 回発火する。値は要素から
|
|
10916
|
+
* 読む契約なので結果は変わらない。詳細は上記 §2.1 の「`$listKeys` との相互作用」。
|
|
10917
|
+
*/
|
|
10918
|
+
function dispatchBindableEvent(stateElement, pathInfo, detail) {
|
|
10919
|
+
const map = stateElement.bindableEventMap;
|
|
10920
|
+
const exactEventName = map[pathInfo.path];
|
|
10921
|
+
const isExact = typeof exactEventName === "string";
|
|
10922
|
+
const eventName = isExact
|
|
10923
|
+
? exactEventName
|
|
10924
|
+
: (pathInfo.segments.length > 1 ? map[pathInfo.segments[0]] : undefined);
|
|
10925
|
+
if (typeof eventName !== "string") {
|
|
10926
|
+
return;
|
|
10927
|
+
}
|
|
10928
|
+
const rootNode = stateElement.rootNode;
|
|
10929
|
+
if (!(rootNode instanceof ShadowRoot)) {
|
|
10930
|
+
return;
|
|
10931
|
+
}
|
|
10932
|
+
rootNode.host.dispatchEvent(new CustomEvent(eventName, {
|
|
10933
|
+
// 完全一致のときだけ、書き込んだ値をそのまま載せる(従来互換)。
|
|
10934
|
+
detail: isExact && typeof detail !== "undefined" ? detail.value : undefined,
|
|
10935
|
+
bubbles: true,
|
|
10936
|
+
}));
|
|
10937
|
+
}
|
|
10938
|
+
|
|
10939
|
+
/**
|
|
10940
|
+
* list/mergeKeyedList.ts
|
|
10941
|
+
*
|
|
10942
|
+
* `$listKeys` 宣言済みリストパスへの配列代入で、キーが一致する行の
|
|
10943
|
+
* 「オブジェクト強制・値展開」を行う(docs/state-list-key-design.md §2)。
|
|
10944
|
+
*
|
|
10945
|
+
* - キー突合し、一致行は**旧オブジェクトを据え置いた**ハイブリッド配列を作る
|
|
10946
|
+
* → 配列要素の参照が変わらないので for は行を再利用する(DOM・フォーカス・
|
|
10947
|
+
* 非バインド DOM 状態が保存される)
|
|
10948
|
+
* - 一致行の「変化したフィールド」だけを列挙して返す
|
|
10949
|
+
* → 呼び出し側が per-path 書き込みとして発行する(§7.0 の穴を塞ぐ正典イディオム)
|
|
10950
|
+
*
|
|
10951
|
+
* このモジュールは純粋な計算のみで、state への書き込みは行わない。
|
|
10952
|
+
*/
|
|
10953
|
+
/**
|
|
10954
|
+
* 値展開は own enumerable データプロパティのコピーなので、プロトタイプや
|
|
10955
|
+
* アクセサを持つオブジェクトでは意味論が保てない。plain object 以外は即エラー(§5)。
|
|
10956
|
+
*/
|
|
10957
|
+
function assertPlainRow(row, path, side, position) {
|
|
10958
|
+
if (typeof row !== "object" || row === null) {
|
|
10959
|
+
raiseError(`${STATE_LIST_KEYS_NAME} list "${path}": ${side} row at index ${position} must be a plain object ` +
|
|
10960
|
+
`(got ${row === null ? "null" : typeof row}).`);
|
|
10961
|
+
}
|
|
10962
|
+
const proto = Object.getPrototypeOf(row);
|
|
10963
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
10964
|
+
raiseError(`${STATE_LIST_KEYS_NAME} list "${path}": ${side} row at index ${position} must be a plain object ` +
|
|
10965
|
+
`(class instances and exotic objects cannot be value-expanded).`);
|
|
10966
|
+
}
|
|
10967
|
+
}
|
|
10968
|
+
function keyOf(row, spec, path, side, position) {
|
|
10969
|
+
const key = typeof spec === "function" ? spec(row) : row[spec];
|
|
10970
|
+
if (key === undefined || key === null) {
|
|
10971
|
+
raiseError(`${STATE_LIST_KEYS_NAME} list "${path}": ${side} row at index ${position} has no key ` +
|
|
10972
|
+
`(${typeof spec === "function" ? "key function" : `field "${spec}"`} returned ${String(key)}).`);
|
|
10973
|
+
}
|
|
10974
|
+
return key;
|
|
10975
|
+
}
|
|
10976
|
+
/** 行を検証しつつキーを抽出する。キー重複は即エラー(§5)。 */
|
|
10977
|
+
function extractKeys(list, spec, path, side) {
|
|
10978
|
+
const keys = new Array(list.length);
|
|
10979
|
+
const seen = new Set();
|
|
10980
|
+
for (let i = 0; i < list.length; i++) {
|
|
10981
|
+
const row = list[i];
|
|
10982
|
+
assertPlainRow(row, path, side, i);
|
|
10983
|
+
const key = keyOf(row, spec, path, side, i);
|
|
10984
|
+
if (seen.has(key)) {
|
|
10985
|
+
raiseError(`${STATE_LIST_KEYS_NAME} list "${path}": duplicate key ${JSON.stringify(key)} in ${side} list.`);
|
|
10986
|
+
}
|
|
10987
|
+
seen.add(key);
|
|
10988
|
+
keys[i] = key;
|
|
10989
|
+
}
|
|
10990
|
+
return keys;
|
|
10991
|
+
}
|
|
10992
|
+
/**
|
|
10993
|
+
* キー突合してハイブリッド配列を組む。値展開すべき一致行が 1 つも無ければ null
|
|
10994
|
+
* (呼び出し側は従来どおりの書き込みへ倒す)。
|
|
10995
|
+
*
|
|
10996
|
+
* 突合対象の旧配列は「最後に適用された配列」ではなく**現在格納されている配列**。
|
|
10997
|
+
* ハイブリッド構築が格納配列の参照を保存するため、同一マイクロタスク内の連続
|
|
10998
|
+
* 書き込みでも適用時の diff と transitive に整合する(§6)。
|
|
10999
|
+
*/
|
|
11000
|
+
function mergeKeyedList(path, spec, oldValue, newList) {
|
|
11001
|
+
// 宣言済みパスなら初回代入(旧配列なし)でも新配列を検証する。
|
|
11002
|
+
// 「2 回目の代入で初めて重複キーが露見する」という不連続を避けるため。
|
|
11003
|
+
const newKeys = extractKeys(newList, spec, path, "new");
|
|
11004
|
+
if (!Array.isArray(oldValue) || oldValue.length === 0 || newList.length === 0) {
|
|
11005
|
+
return null;
|
|
11006
|
+
}
|
|
11007
|
+
const oldList = oldValue;
|
|
11008
|
+
const oldKeys = extractKeys(oldList, spec, path, "current");
|
|
11009
|
+
const oldRowByKey = new Map();
|
|
11010
|
+
for (let i = 0; i < oldList.length; i++) {
|
|
11011
|
+
oldRowByKey.set(oldKeys[i], oldList[i]);
|
|
11012
|
+
}
|
|
11013
|
+
const list = new Array(newList.length);
|
|
11014
|
+
const matched = [];
|
|
11015
|
+
for (let i = 0; i < newList.length; i++) {
|
|
11016
|
+
const newRow = newList[i];
|
|
11017
|
+
const oldRow = oldRowByKey.get(newKeys[i]);
|
|
11018
|
+
if (typeof oldRow === "undefined" || oldRow === newRow) {
|
|
11019
|
+
// 追加行、または既に同一オブジェクト(生配列 in-place 変異 + コピー再代入の
|
|
11020
|
+
// イディオム)。後者は従来どおり walkDependency の全行フォールバックが担う。
|
|
11021
|
+
list[i] = newRow;
|
|
11022
|
+
continue;
|
|
11023
|
+
}
|
|
11024
|
+
list[i] = oldRow;
|
|
11025
|
+
matched.push({ position: i, oldRow, newRow });
|
|
11026
|
+
}
|
|
11027
|
+
return matched.length > 0 ? { list, matched } : null;
|
|
11028
|
+
}
|
|
11029
|
+
/**
|
|
11030
|
+
* 一致行について per-path 書き込みすべきフィールドを列挙する。
|
|
11031
|
+
*
|
|
11032
|
+
* - 変化したフィールドのみ(同値は書かない。無変化リフレッシュを完全なゼロコストに
|
|
11033
|
+
* するため — 全フィールド無条件書き込みだと §2.2 の利得が消える)
|
|
11034
|
+
* - 新行から消えた旧フィールドは **null** を書く(undefined ではない)
|
|
11035
|
+
*
|
|
11036
|
+
* 同値判定は Object.is。setByAddress の same-value guard と同じ基準にすることで、
|
|
11037
|
+
* 「発行したが guard に落とされる」無駄な書き込みを作らない。
|
|
11038
|
+
*
|
|
11039
|
+
* 消えたフィールドに null を使うのは、この処理系では undefined が
|
|
11040
|
+
* 「状態が値を持たない=無意見」であり applyChangeToProperty が書き込みごと
|
|
11041
|
+
* スキップするため(明示的なクリアの語彙は null)。undefined を書くと state 側は
|
|
11042
|
+
* 更新されるのに DOM だけ旧値のまま残り、まさに本機能が塞ごうとしている
|
|
11043
|
+
* stale を再導入してしまう。既に null / undefined のフィールドは DOM 上も
|
|
11044
|
+
* 空なので、クリア書き込み自体を発行しない。
|
|
11045
|
+
*/
|
|
11046
|
+
function collectFieldWrites(oldRow, newRow) {
|
|
11047
|
+
const writes = [];
|
|
11048
|
+
for (const field of Object.keys(newRow)) {
|
|
11049
|
+
if (!Object.is(oldRow[field], newRow[field])) {
|
|
11050
|
+
writes.push({ field, value: newRow[field] });
|
|
11051
|
+
}
|
|
11052
|
+
}
|
|
11053
|
+
for (const field of Object.keys(oldRow)) {
|
|
11054
|
+
if (!Object.hasOwn(newRow, field) && oldRow[field] != null) {
|
|
11055
|
+
writes.push({ field, value: null });
|
|
11056
|
+
}
|
|
11057
|
+
}
|
|
11058
|
+
return writes;
|
|
9935
11059
|
}
|
|
9936
11060
|
|
|
9937
11061
|
/**
|
|
@@ -9970,6 +11094,108 @@ function setSwapInfoByAddress(address, swapInfo) {
|
|
|
9970
11094
|
}
|
|
9971
11095
|
}
|
|
9972
11096
|
|
|
11097
|
+
/**
|
|
11098
|
+
* topologicalRank.ts — 依存グラフ(パス単位)のトポロジカル順位。
|
|
11099
|
+
*
|
|
11100
|
+
* 依存ウォークは list → list.* を展開するために途中でリスト実体を読む。この読み取りが
|
|
11101
|
+
* 正しい値を返すには「そのパスの入力(先行パス)がすべて dirty 化済み」である必要がある。
|
|
11102
|
+
* DFS ではダイヤモンド依存で片腕しか dirty 化していない段階で合流点を評価してしまうため、
|
|
11103
|
+
* パス単位の rank(= 最長経路長)を先に求め、rank の昇順で訪問する。
|
|
11104
|
+
*
|
|
11105
|
+
* rank の定義から、辺 (u → v) が存在すれば必ず rank(u) < rank(v) となる。したがって
|
|
11106
|
+
* rank r のバケットを処理する時点で rank < r のパスはすべて訪問(dirty 化)済みであり、
|
|
11107
|
+
* 同じバケット内のパス同士は互いに先行関係を持たない。
|
|
11108
|
+
*
|
|
11109
|
+
* 値を読まないグラフ走査なので、ウォーク 1 回あたりの追加コストは実測で誤差に
|
|
11110
|
+
* 収まる(メモ化しても差が出なかったため、キャッシュは持たない)。
|
|
11111
|
+
*/
|
|
11112
|
+
function getTopologicalRanks(startPath, staticMap, dynamicMap, maxDepth) {
|
|
11113
|
+
// 1) startPath から到達可能なパス部分グラフと入次数を求める(値は一切読まない)
|
|
11114
|
+
const adjacency = new Map();
|
|
11115
|
+
const inDegree = new Map();
|
|
11116
|
+
const pending = [startPath];
|
|
11117
|
+
inDegree.set(startPath, 0);
|
|
11118
|
+
while (pending.length > 0) {
|
|
11119
|
+
const path = pending.pop();
|
|
11120
|
+
if (adjacency.has(path)) {
|
|
11121
|
+
continue;
|
|
11122
|
+
}
|
|
11123
|
+
const staticDeps = staticMap.get(path);
|
|
11124
|
+
const dynamicDeps = dynamicMap.get(path);
|
|
11125
|
+
let deps;
|
|
11126
|
+
if (staticDeps === undefined) {
|
|
11127
|
+
deps = dynamicDeps ?? [];
|
|
11128
|
+
}
|
|
11129
|
+
else if (dynamicDeps === undefined) {
|
|
11130
|
+
deps = staticDeps;
|
|
11131
|
+
}
|
|
11132
|
+
else {
|
|
11133
|
+
deps = staticDeps.concat(dynamicDeps);
|
|
11134
|
+
}
|
|
11135
|
+
adjacency.set(path, deps);
|
|
11136
|
+
for (let i = 0; i < deps.length; i++) {
|
|
11137
|
+
const dep = deps[i];
|
|
11138
|
+
inDegree.set(dep, (inDegree.get(dep) ?? 0) + 1);
|
|
11139
|
+
if (!adjacency.has(dep)) {
|
|
11140
|
+
pending.push(dep);
|
|
11141
|
+
}
|
|
11142
|
+
}
|
|
11143
|
+
}
|
|
11144
|
+
// 2) Kahn 法。rank は最長経路長(rank[v] = max(rank[u]) + 1)。
|
|
11145
|
+
// 入次数が 0 に落ちて queue に入ったパスだけが「確定」で、緩和の途中で
|
|
11146
|
+
// 暫定値が入っただけのパス(= 循環の一部)は確定扱いにしない。
|
|
11147
|
+
const ranks = new Map();
|
|
11148
|
+
const settled = new Set();
|
|
11149
|
+
const queue = [];
|
|
11150
|
+
for (const [path, degree] of inDegree) {
|
|
11151
|
+
if (degree === 0) {
|
|
11152
|
+
ranks.set(path, 0);
|
|
11153
|
+
settled.add(path);
|
|
11154
|
+
queue.push(path);
|
|
11155
|
+
}
|
|
11156
|
+
}
|
|
11157
|
+
for (let i = 0; i < queue.length; i++) {
|
|
11158
|
+
const path = queue[i];
|
|
11159
|
+
const nextRank = ranks.get(path) + 1;
|
|
11160
|
+
if (nextRank > maxDepth) {
|
|
11161
|
+
raiseError(`Maximum dependency depth of ${maxDepth} exceeded. Possible circular dependency detected at path: ${path}`);
|
|
11162
|
+
}
|
|
11163
|
+
const deps = adjacency.get(path);
|
|
11164
|
+
for (let j = 0; j < deps.length; j++) {
|
|
11165
|
+
const dep = deps[j];
|
|
11166
|
+
if (nextRank > (ranks.get(dep) ?? -1)) {
|
|
11167
|
+
ranks.set(dep, nextRank);
|
|
11168
|
+
}
|
|
11169
|
+
const remaining = inDegree.get(dep) - 1;
|
|
11170
|
+
inDegree.set(dep, remaining);
|
|
11171
|
+
if (remaining === 0) {
|
|
11172
|
+
settled.add(dep);
|
|
11173
|
+
queue.push(dep);
|
|
11174
|
+
}
|
|
11175
|
+
}
|
|
11176
|
+
}
|
|
11177
|
+
// 3) 循環に含まれるパスは rank が決まらない(入次数が 0 に落ちない)。
|
|
11178
|
+
// そもそも正しい評価順が存在しないので、順序保証を諦めて確定済みの
|
|
11179
|
+
// 最大 rank の次にまとめる。打ち切りは従来どおり visited が担う。
|
|
11180
|
+
// 暫定値が残っていると確定パスとの前後関係を誤って表すため、必ず上書きする。
|
|
11181
|
+
if (settled.size !== adjacency.size) {
|
|
11182
|
+
let maxRank = -1;
|
|
11183
|
+
for (const path of settled) {
|
|
11184
|
+
const rank = ranks.get(path);
|
|
11185
|
+
if (rank > maxRank) {
|
|
11186
|
+
maxRank = rank;
|
|
11187
|
+
}
|
|
11188
|
+
}
|
|
11189
|
+
const cycleRank = maxRank + 1;
|
|
11190
|
+
for (const path of adjacency.keys()) {
|
|
11191
|
+
if (!settled.has(path)) {
|
|
11192
|
+
ranks.set(path, cycleRank);
|
|
11193
|
+
}
|
|
11194
|
+
}
|
|
11195
|
+
}
|
|
11196
|
+
return ranks;
|
|
11197
|
+
}
|
|
11198
|
+
|
|
9973
11199
|
const MAX_DEPENDENCY_DEPTH = 1000;
|
|
9974
11200
|
function getIndexes(listDiff, searchType) {
|
|
9975
11201
|
switch (searchType) {
|
|
@@ -9998,7 +11224,7 @@ function _walkExpandWildcard(context, currentWildcardIndex, parentListIndex) {
|
|
|
9998
11224
|
const parentAbsAddress = createAbsoluteStateAddress(parentAbsPathInfo, parentListIndex);
|
|
9999
11225
|
const lastValue = getLastListValueByAbsoluteStateAddress(parentAbsAddress);
|
|
10000
11226
|
const newValue = context.stateProxy[getByAddressSymbol](parentAddress);
|
|
10001
|
-
const listDiff = createListDiff(parentAddress.listIndex, lastValue, newValue);
|
|
11227
|
+
const listDiff = createListDiff(getListParentListIndex(context.stateElement, parentAddress.listIndex), lastValue, newValue);
|
|
10002
11228
|
const loopIndexes = getIndexes(listDiff, context.searchType);
|
|
10003
11229
|
if (currentWildcardIndex === context.wildcardPaths.length - 1) {
|
|
10004
11230
|
context.targetListIndexes.push(...loopIndexes);
|
|
@@ -10028,6 +11254,12 @@ function selectExpansionIndexes(context, sourcePath, _lastValue, _newValue, list
|
|
|
10028
11254
|
if (listDiff.addIndexSet.size === 0 && listDiff.changeIndexSet.size === 0) {
|
|
10029
11255
|
// 追加も移動も無い。削除も無ければ「変化が見えない再代入」= リフレッシュ意図
|
|
10030
11256
|
if (listDiff.deleteIndexSet.size === 0) {
|
|
11257
|
+
// ただしキー突合による値展開が成立した書き込みでは、変化フィールドは
|
|
11258
|
+
// per-path 書き込みで個別に dirty 化済み。全行展開は純粋な無駄になる
|
|
11259
|
+
// (無変化ポーリングをゼロコストにする — 設計書 §2.2)。
|
|
11260
|
+
if (context.keyedMergePath === sourcePath) {
|
|
11261
|
+
return { fullRows: EMPTY_INDEXES, movedRows: null };
|
|
11262
|
+
}
|
|
10031
11263
|
return { fullRows: listDiff.newIndexes, movedRows: null };
|
|
10032
11264
|
}
|
|
10033
11265
|
// 削除のみ: 残存行は位置も値も不変なので展開しない
|
|
@@ -10076,152 +11308,178 @@ function getMovedRowExpansionPaths(context, wildcardPath, depPathInfo) {
|
|
|
10076
11308
|
return result ?? EMPTY_PATH_INFOS;
|
|
10077
11309
|
}
|
|
10078
11310
|
function _walkDependency(context, startAddress, callback) {
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
11311
|
+
// rank ごとのバケットで訪問する。辺 (u → v) では必ず rank(u) < rank(v) なので、
|
|
11312
|
+
// バケット r を処理する時点で rank < r のパスは全て dirty 化済みになる
|
|
11313
|
+
// = ここでリスト実体を読んでも入力が揃っている(topologicalRank.ts 参照)。
|
|
11314
|
+
const buckets = [];
|
|
11315
|
+
const ranks = context.ranks;
|
|
11316
|
+
const enqueue = (address, minRank) => {
|
|
11317
|
+
let rank = ranks.get(address.pathInfo.path) ?? minRank;
|
|
11318
|
+
if (rank < minRank) {
|
|
11319
|
+
// 循環など rank が先行関係を表せないケース。現在のバケットに載せて
|
|
11320
|
+
// 同一ループ内で処理する(打ち切りは visited が担う)。
|
|
11321
|
+
rank = minRank;
|
|
11322
|
+
}
|
|
11323
|
+
(buckets[rank] ??= []).push(address);
|
|
11324
|
+
};
|
|
11325
|
+
enqueue(startAddress, 0);
|
|
11326
|
+
// 依存アドレスを収集するための一時バッファ(アドレスごとに使い回す)
|
|
11327
|
+
const nextEntries = [];
|
|
11328
|
+
for (let rank = 0; rank < buckets.length; rank++) {
|
|
11329
|
+
const bucket = buckets[rank];
|
|
11330
|
+
if (bucket === undefined) {
|
|
10086
11331
|
continue;
|
|
10087
11332
|
}
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
if (staticDeps) {
|
|
10101
|
-
for (const dep of staticDeps) {
|
|
10102
|
-
const depPathInfo = getPathInfo(dep);
|
|
10103
|
-
if (context.listPathSet.has(sourcePath) && depPathInfo.lastSegment === WILDCARD) {
|
|
10104
|
-
//expand indexes
|
|
10105
|
-
const newValue = context.stateProxy[getByAddressSymbol](address);
|
|
10106
|
-
const absPathInfo = getAbsolutePathInfo(context.stateElement, address.pathInfo);
|
|
10107
|
-
const absAddress = createAbsoluteStateAddress(absPathInfo, address.listIndex);
|
|
10108
|
-
const lastValue = getLastListValueByAbsoluteStateAddress(absAddress);
|
|
10109
|
-
const listDiff = createListDiff(address.listIndex, lastValue, newValue);
|
|
10110
|
-
const selection = selectExpansionIndexes(context, sourcePath, lastValue, newValue, listDiff);
|
|
10111
|
-
for (const listIndex of selection.fullRows) {
|
|
10112
|
-
const depAddress = createStateAddress(depPathInfo, listIndex);
|
|
10113
|
-
context.result.add(depAddress);
|
|
10114
|
-
nextEntries.push({ address: depAddress, depth: nextDepth });
|
|
10115
|
-
}
|
|
10116
|
-
if (selection.movedRows !== null) {
|
|
10117
|
-
const movedPathInfos = getMovedRowExpansionPaths(context, dep, depPathInfo);
|
|
10118
|
-
if (movedPathInfos === null) {
|
|
10119
|
-
// ネスト配下に index 依存 getter: 安全側で行全体を展開(従来挙動)
|
|
10120
|
-
for (const listIndex of selection.movedRows) {
|
|
10121
|
-
const depAddress = createStateAddress(depPathInfo, listIndex);
|
|
10122
|
-
context.result.add(depAddress);
|
|
10123
|
-
nextEntries.push({ address: depAddress, depth: nextDepth });
|
|
10124
|
-
}
|
|
10125
|
-
}
|
|
10126
|
-
else if (movedPathInfos.length > 0) {
|
|
10127
|
-
// 位置のみ変わった行は index 依存 getter のパスだけを展開する
|
|
10128
|
-
for (const listIndex of selection.movedRows) {
|
|
10129
|
-
for (const pathInfo of movedPathInfos) {
|
|
10130
|
-
const depAddress = createStateAddress(pathInfo, listIndex);
|
|
10131
|
-
context.result.add(depAddress);
|
|
10132
|
-
nextEntries.push({ address: depAddress, depth: nextDepth });
|
|
10133
|
-
}
|
|
10134
|
-
}
|
|
10135
|
-
}
|
|
10136
|
-
// movedPathInfos が空: index を読む getter が subtree に無い =
|
|
10137
|
-
// 位置のみ変わった行の値は不変。展開・dirty 化とも不要。
|
|
10138
|
-
}
|
|
10139
|
-
}
|
|
10140
|
-
else {
|
|
10141
|
-
const depAddress = createStateAddress(depPathInfo, address.listIndex);
|
|
10142
|
-
context.result.add(depAddress);
|
|
10143
|
-
nextEntries.push({ address: depAddress, depth: nextDepth });
|
|
10144
|
-
}
|
|
11333
|
+
// 同一バケットへの push(循環時)で伸びるため length は都度読む
|
|
11334
|
+
for (let cursor = 0; cursor < bucket.length; cursor++) {
|
|
11335
|
+
const address = bucket[cursor];
|
|
11336
|
+
if (context.visited.has(address)) {
|
|
11337
|
+
continue;
|
|
11338
|
+
}
|
|
11339
|
+
context.visited.add(address);
|
|
11340
|
+
callback(address);
|
|
11341
|
+
nextEntries.length = 0;
|
|
11342
|
+
_collectDependencies(context, address, nextEntries);
|
|
11343
|
+
for (let i = 0; i < nextEntries.length; i++) {
|
|
11344
|
+
enqueue(nextEntries[i], rank + 1);
|
|
10145
11345
|
}
|
|
10146
11346
|
}
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
11347
|
+
}
|
|
11348
|
+
}
|
|
11349
|
+
/**
|
|
11350
|
+
* address の依存アドレスを nextEntries に集め、context.result にも登録する。
|
|
11351
|
+
* リスト展開(list → list.*)と動的依存のワイルドカード展開はここで値を読むが、
|
|
11352
|
+
* 呼び出し元がトポロジカル順を保証しているため入力は揃っている。
|
|
11353
|
+
*/
|
|
11354
|
+
function _collectDependencies(context, address, nextEntries) {
|
|
11355
|
+
const sourcePath = address.pathInfo.path;
|
|
11356
|
+
/**
|
|
11357
|
+
* パスから依存関係をたどる
|
|
11358
|
+
* users.*.name <= users.* <= users
|
|
11359
|
+
* ただし、users がリストであれば users.* の依存関係は展開する
|
|
11360
|
+
*/
|
|
11361
|
+
const staticDeps = context.staticMap.get(sourcePath);
|
|
11362
|
+
if (staticDeps) {
|
|
11363
|
+
for (const dep of staticDeps) {
|
|
11364
|
+
const depPathInfo = getPathInfo(dep);
|
|
11365
|
+
if (context.listPathSet.has(sourcePath) && depPathInfo.lastSegment === WILDCARD) {
|
|
11366
|
+
//expand indexes
|
|
11367
|
+
const newValue = context.stateProxy[getByAddressSymbol](address);
|
|
11368
|
+
const absPathInfo = getAbsolutePathInfo(context.stateElement, address.pathInfo);
|
|
11369
|
+
const absAddress = createAbsoluteStateAddress(absPathInfo, address.listIndex);
|
|
11370
|
+
const lastValue = getLastListValueByAbsoluteStateAddress(absAddress);
|
|
11371
|
+
const listDiff = createListDiff(getListParentListIndex(context.stateElement, address.listIndex), lastValue, newValue);
|
|
11372
|
+
const selection = selectExpansionIndexes(context, sourcePath, lastValue, newValue, listDiff);
|
|
11373
|
+
for (const listIndex of selection.fullRows) {
|
|
11374
|
+
const depAddress = createStateAddress(depPathInfo, listIndex);
|
|
11375
|
+
context.result.add(depAddress);
|
|
11376
|
+
nextEntries.push(depAddress);
|
|
11377
|
+
}
|
|
11378
|
+
if (selection.movedRows !== null) {
|
|
11379
|
+
const movedPathInfos = getMovedRowExpansionPaths(context, dep, depPathInfo);
|
|
11380
|
+
if (movedPathInfos === null) {
|
|
11381
|
+
// ネスト配下に index 依存 getter: 安全側で行全体を展開(従来挙動)
|
|
11382
|
+
for (const listIndex of selection.movedRows) {
|
|
11383
|
+
const depAddress = createStateAddress(depPathInfo, listIndex);
|
|
11384
|
+
context.result.add(depAddress);
|
|
11385
|
+
nextEntries.push(depAddress);
|
|
10183
11386
|
}
|
|
10184
|
-
|
|
10185
|
-
|
|
10186
|
-
|
|
10187
|
-
|
|
11387
|
+
}
|
|
11388
|
+
else if (movedPathInfos.length > 0) {
|
|
11389
|
+
// 位置のみ変わった行は index 依存 getter のパスだけを展開する
|
|
11390
|
+
for (const listIndex of selection.movedRows) {
|
|
11391
|
+
for (const pathInfo of movedPathInfos) {
|
|
11392
|
+
const depAddress = createStateAddress(pathInfo, listIndex);
|
|
11393
|
+
context.result.add(depAddress);
|
|
11394
|
+
nextEntries.push(depAddress);
|
|
11395
|
+
}
|
|
10188
11396
|
}
|
|
10189
|
-
const expandContext = {
|
|
10190
|
-
stateElement: context.stateElement,
|
|
10191
|
-
targetListIndexes: [],
|
|
10192
|
-
wildcardPaths: depPathInfo.wildcardPaths,
|
|
10193
|
-
wildcardParentPaths: depPathInfo.wildcardParentPaths,
|
|
10194
|
-
stateProxy: context.stateProxy,
|
|
10195
|
-
searchType: context.searchType,
|
|
10196
|
-
};
|
|
10197
|
-
_walkExpandWildcard(expandContext, wildcardLen, listIndex);
|
|
10198
|
-
listIndexes.push(...expandContext.targetListIndexes);
|
|
10199
11397
|
}
|
|
10200
|
-
|
|
10201
|
-
|
|
10202
|
-
|
|
11398
|
+
// movedPathInfos が空: index を読む getter が subtree に無い =
|
|
11399
|
+
// 位置のみ変わった行の値は不変。展開・dirty 化とも不要。
|
|
11400
|
+
}
|
|
11401
|
+
}
|
|
11402
|
+
else {
|
|
11403
|
+
const depAddress = createStateAddress(depPathInfo, address.listIndex);
|
|
11404
|
+
context.result.add(depAddress);
|
|
11405
|
+
nextEntries.push(depAddress);
|
|
11406
|
+
}
|
|
11407
|
+
}
|
|
11408
|
+
}
|
|
11409
|
+
/**
|
|
11410
|
+
* 動的依存関係をたどる
|
|
11411
|
+
* 動的依存関係は、getterの実行時に決定される
|
|
11412
|
+
*
|
|
11413
|
+
* source, target
|
|
11414
|
+
*
|
|
11415
|
+
* products.*.price => products.*.tax
|
|
11416
|
+
* get "products.*.tax"() { return this["products.*.price"] * 0.1; }
|
|
11417
|
+
*
|
|
11418
|
+
* products.*.price => products.summary
|
|
11419
|
+
* get "products.summary"() { return this.$getAll("products.*.price", []).reduce(sum); }
|
|
11420
|
+
*
|
|
11421
|
+
* categories.*.name => categories.*.products.*.categoryName
|
|
11422
|
+
* get "categories.*.products.*.categoryName"() { return this["categories.*.name"]; }
|
|
11423
|
+
*/
|
|
11424
|
+
const dynamicDeps = context.dynamicMap.get(sourcePath);
|
|
11425
|
+
if (dynamicDeps) {
|
|
11426
|
+
for (const dep of dynamicDeps) {
|
|
11427
|
+
const depPathInfo = getPathInfo(dep);
|
|
11428
|
+
const listIndexes = [];
|
|
11429
|
+
if (depPathInfo.wildcardCount > 0) {
|
|
11430
|
+
// ワイルドカードを含む依存関係の処理
|
|
11431
|
+
// 同じ親を持つかをパスの集合積で判定する
|
|
11432
|
+
// polyfills.tsにてSetのintersectionメソッドを定義している
|
|
11433
|
+
const wildcardLen = calcWildcardLen(address.pathInfo, depPathInfo);
|
|
11434
|
+
const expandable = (depPathInfo.wildcardCount - wildcardLen) >= 1;
|
|
11435
|
+
if (expandable) {
|
|
11436
|
+
let listIndex;
|
|
11437
|
+
if (wildcardLen > 0) {
|
|
11438
|
+
// categories.*.name => categories.*.products.*.categoryName
|
|
11439
|
+
// ワイルドカードを含む同じ親(products.*)を持つのが、
|
|
11440
|
+
// さらに下位にワイルドカードがあるので展開する
|
|
10203
11441
|
if (address.listIndex === null) {
|
|
10204
11442
|
raiseError(`Cannot expand dynamic dependency with wildcard for non-list address: ${address.pathInfo.path}`);
|
|
10205
11443
|
}
|
|
10206
|
-
|
|
10207
|
-
listIndexes.push(listIndex);
|
|
11444
|
+
listIndex = listIndexAtWildcard(address.listIndex, wildcardLen - 1, address.pathInfo.wildcardCount);
|
|
10208
11445
|
}
|
|
11446
|
+
else {
|
|
11447
|
+
// selectedIndex => items.*.selected
|
|
11448
|
+
// 同じ親を持たない場合はnullから開始
|
|
11449
|
+
listIndex = null;
|
|
11450
|
+
}
|
|
11451
|
+
const expandContext = {
|
|
11452
|
+
stateName: context.stateName,
|
|
11453
|
+
stateElement: context.stateElement,
|
|
11454
|
+
targetListIndexes: [],
|
|
11455
|
+
wildcardPaths: depPathInfo.wildcardPaths,
|
|
11456
|
+
wildcardParentPaths: depPathInfo.wildcardParentPaths,
|
|
11457
|
+
stateProxy: context.stateProxy,
|
|
11458
|
+
searchType: context.searchType,
|
|
11459
|
+
};
|
|
11460
|
+
_walkExpandWildcard(expandContext, wildcardLen, listIndex);
|
|
11461
|
+
listIndexes.push(...expandContext.targetListIndexes);
|
|
10209
11462
|
}
|
|
10210
11463
|
else {
|
|
10211
|
-
// products.*.
|
|
10212
|
-
//
|
|
10213
|
-
|
|
10214
|
-
|
|
10215
|
-
|
|
10216
|
-
const
|
|
10217
|
-
|
|
10218
|
-
nextEntries.push({ address: depAddress, depth: nextDepth });
|
|
11464
|
+
// products.*.price => products.*.tax
|
|
11465
|
+
// ワイルドカードを含む同じ親(products.*)を持つので、リストインデックスは引き継ぐ
|
|
11466
|
+
if (address.listIndex === null) {
|
|
11467
|
+
raiseError(`Cannot expand dynamic dependency with wildcard for non-list address: ${address.pathInfo.path}`);
|
|
11468
|
+
}
|
|
11469
|
+
const listIndex = listIndexAtWildcard(address.listIndex, wildcardLen - 1, address.pathInfo.wildcardCount);
|
|
11470
|
+
listIndexes.push(listIndex);
|
|
10219
11471
|
}
|
|
10220
11472
|
}
|
|
10221
|
-
|
|
10222
|
-
|
|
10223
|
-
|
|
10224
|
-
|
|
11473
|
+
else {
|
|
11474
|
+
// products.*.tax => currentTaxRate
|
|
11475
|
+
// 同じ親を持たないので、リストインデックスはnull
|
|
11476
|
+
listIndexes.push(null);
|
|
11477
|
+
}
|
|
11478
|
+
for (const listIndex of listIndexes) {
|
|
11479
|
+
const depAddress = createStateAddress(depPathInfo, listIndex);
|
|
11480
|
+
context.result.add(depAddress);
|
|
11481
|
+
nextEntries.push(depAddress);
|
|
11482
|
+
}
|
|
10225
11483
|
}
|
|
10226
11484
|
}
|
|
10227
11485
|
}
|
|
@@ -10235,7 +11493,12 @@ function walkDependency(stateName, stateElement, startAddress, staticDependency,
|
|
|
10235
11493
|
callback(startAddress);
|
|
10236
11494
|
return [];
|
|
10237
11495
|
}
|
|
11496
|
+
// パス単位のトポロジカル順位。値を一切読まずに求まり、依存グラフは追記のみで
|
|
11497
|
+
// 成長するため epoch でメモ化される(topologicalRank.ts)。
|
|
11498
|
+
const ranks = getTopologicalRanks(startPath, staticDependency, dynamicDependency, MAX_DEPENDENCY_DEPTH);
|
|
10238
11499
|
const context = {
|
|
11500
|
+
ranks: ranks,
|
|
11501
|
+
stateName: stateName,
|
|
10239
11502
|
stateElement: stateElement,
|
|
10240
11503
|
staticMap: staticDependency,
|
|
10241
11504
|
dynamicMap: dynamicDependency,
|
|
@@ -10245,6 +11508,7 @@ function walkDependency(stateName, stateElement, startAddress, staticDependency,
|
|
|
10245
11508
|
stateProxy: stateProxy,
|
|
10246
11509
|
searchType: searchType,
|
|
10247
11510
|
listExpansion: options?.listExpansion ?? "full",
|
|
11511
|
+
keyedMergePath: options?.keyedMergePath ?? null,
|
|
10248
11512
|
};
|
|
10249
11513
|
_walkDependency(context, startAddress, callback);
|
|
10250
11514
|
return Array.from(context.result);
|
|
@@ -10271,7 +11535,7 @@ function walkDependency(stateName, stateElement, startAddress, staticDependency,
|
|
|
10271
11535
|
// binding 経由の書き込みは呼び出し元の dynamic scope から context を引き継ぎ、
|
|
10272
11536
|
// binding 外からの API update は新しい transaction を開始する(設計書 §4 規則 1)。
|
|
10273
11537
|
// 依存 walk で enqueue される派生アドレスも同じ書き込みの因果に属する。
|
|
10274
|
-
function notifyWrite(address, absAddress, receiver, handler) {
|
|
11538
|
+
function notifyWrite(address, absAddress, receiver, handler, keyedMergePath) {
|
|
10275
11539
|
const propagationContext = config.enablePropagationContext
|
|
10276
11540
|
? (getCurrentPropagationContext() ?? beginPropagationTransaction(-1))
|
|
10277
11541
|
: null;
|
|
@@ -10290,9 +11554,9 @@ function notifyWrite(address, absAddress, receiver, handler) {
|
|
|
10290
11554
|
},
|
|
10291
11555
|
// リスト置換時は追加行・位置変更行のみ展開する(未変更行の再訪を省く。
|
|
10292
11556
|
// $postUpdate の手動リフレッシュは従来通り全行展開のまま)
|
|
10293
|
-
{ listExpansion: "diff" });
|
|
11557
|
+
{ listExpansion: "diff", keyedMergePath });
|
|
10294
11558
|
}
|
|
10295
|
-
function _setByAddress(target, address, absAddress, value, receiver, handler) {
|
|
11559
|
+
function _setByAddress(target, address, absAddress, value, receiver, handler, keyedMergePath) {
|
|
10296
11560
|
try {
|
|
10297
11561
|
if (address.pathInfo.path in target) {
|
|
10298
11562
|
if (handler.stateElement.setterPaths.has(address.pathInfo.path)) {
|
|
@@ -10311,6 +11575,17 @@ function _setByAddress(target, address, absAddress, value, receiver, handler) {
|
|
|
10311
11575
|
handler.popAddress();
|
|
10312
11576
|
}
|
|
10313
11577
|
}
|
|
11578
|
+
else if (handler.stateElement.hasMappedComponentState === true) {
|
|
11579
|
+
// target は innerState proxy。set トラップにはパス文字列しか渡らないので、
|
|
11580
|
+
// 解決済みの listIndex を動的スコープで越境させる(§1.8)
|
|
11581
|
+
pushCrossBoundaryAddress(handler.stateElement, address);
|
|
11582
|
+
try {
|
|
11583
|
+
return Reflect.set(target, address.pathInfo.path, value);
|
|
11584
|
+
}
|
|
11585
|
+
finally {
|
|
11586
|
+
popCrossBoundaryAddress();
|
|
11587
|
+
}
|
|
11588
|
+
}
|
|
10314
11589
|
else {
|
|
10315
11590
|
return Reflect.set(target, address.pathInfo.path, value);
|
|
10316
11591
|
}
|
|
@@ -10332,10 +11607,10 @@ function _setByAddress(target, address, absAddress, value, receiver, handler) {
|
|
|
10332
11607
|
}
|
|
10333
11608
|
}
|
|
10334
11609
|
finally {
|
|
10335
|
-
notifyWrite(address, absAddress, receiver, handler);
|
|
11610
|
+
notifyWrite(address, absAddress, receiver, handler, keyedMergePath);
|
|
10336
11611
|
}
|
|
10337
11612
|
}
|
|
10338
|
-
function _setByAddressWithSwap(target, address, absAddress, value, receiver, handler) {
|
|
11613
|
+
function _setByAddressWithSwap(target, address, absAddress, value, receiver, handler, keyedMergePath) {
|
|
10339
11614
|
// elementsの場合はswapInfoを準備
|
|
10340
11615
|
let parentAddress = address.parentAddress ?? raiseError(`address.parentAddress is undefined path: ${address.pathInfo.path}`);
|
|
10341
11616
|
let swapInfo = getSwapInfoByAddress(parentAddress);
|
|
@@ -10348,7 +11623,7 @@ function _setByAddressWithSwap(target, address, absAddress, value, receiver, han
|
|
|
10348
11623
|
setSwapInfoByAddress(parentAddress, swapInfo);
|
|
10349
11624
|
}
|
|
10350
11625
|
try {
|
|
10351
|
-
return _setByAddress(target, address, absAddress, value, receiver, handler);
|
|
11626
|
+
return _setByAddress(target, address, absAddress, value, receiver, handler, keyedMergePath);
|
|
10352
11627
|
}
|
|
10353
11628
|
finally {
|
|
10354
11629
|
const index = swapInfo.value.indexOf(value);
|
|
@@ -10371,7 +11646,80 @@ function _setByAddressWithSwap(target, address, absAddress, value, receiver, han
|
|
|
10371
11646
|
}
|
|
10372
11647
|
}
|
|
10373
11648
|
}
|
|
11649
|
+
/**
|
|
11650
|
+
* `$listKeys` 宣言済みリストパスへの配列代入を「キー一致行のオブジェクト値展開」に
|
|
11651
|
+
* 変換する(docs/state-list-key-design.md §2)。
|
|
11652
|
+
*
|
|
11653
|
+
* 1. キー突合して、一致行は旧オブジェクトを据え置いたハイブリッド配列を作る
|
|
11654
|
+
* 2. ハイブリッド配列を通常の書き込み経路で格納する
|
|
11655
|
+
* 3. createListDiff で listIndex を確定し、変化フィールドだけを per-path 書き込みで発行
|
|
11656
|
+
*
|
|
11657
|
+
* 3 を格納後に行うのが要点。フィールド書き込みは `list.*.field` を親経由で解決する
|
|
11658
|
+
* ため、親(ハイブリッド配列)が既に格納されていなければ正しい行に届かない。
|
|
11659
|
+
* また per-path 書き込みは再び setByAddress に入るので、ネストしたリストパスが
|
|
11660
|
+
* 宣言されていればそのレベルのキー突合が再帰的に走る(§4)。
|
|
11661
|
+
*
|
|
11662
|
+
* 未宣言時のコストは stateElement.listKeys の null 判定 1 回のみ(§7-1)。
|
|
11663
|
+
*/
|
|
11664
|
+
function setKeyedListByAddress(target, address, merge, oldList, receiver, handler) {
|
|
11665
|
+
const listPath = address.pathInfo.path;
|
|
11666
|
+
// diff の基準は「マージ相手にした配列」= 書き込み直前に格納されていた配列。
|
|
11667
|
+
// 読み手(applyChangeToFor / $getAll / resolve)は現在格納されている配列の
|
|
11668
|
+
// listIndex 台帳(listIndexesByList)へ収束するため、同じ基準で引くことで
|
|
11669
|
+
// 書き込みが dirty 化・キャッシュするアドレスと読み手のアドレスが一致する。
|
|
11670
|
+
// lastValue(最後に *適用* された配列)を基準にすると、for が未マウントで
|
|
11671
|
+
// lastValue が空のときに別台帳を作ってしまい、値は入っているのにワイルドカード
|
|
11672
|
+
// 読みだけ旧値のまま残る(設計書 §8.1)。
|
|
11673
|
+
// 格納より前に引くのは、格納時の walkDependency(listExpansion: "diff")が
|
|
11674
|
+
// 先にハイブリッド配列の台帳を作ってしまうと、後から上書きした台帳との間で
|
|
11675
|
+
// 同じ分裂が起きるため。先に確定させておけば以降は全経路がこれに合流する。
|
|
11676
|
+
const listParentListIndex = getListParentListIndex(handler.stateElement, address.listIndex);
|
|
11677
|
+
if (getListIndexesByList(oldList) === null) {
|
|
11678
|
+
// 一度も描画されていないリストは台帳自体が無い。先に生やしておかないと
|
|
11679
|
+
// isSameList 経路が空の oldIndexes をそのまま新台帳にしてしまう。
|
|
11680
|
+
createListDiff(listParentListIndex, null, oldList);
|
|
11681
|
+
}
|
|
11682
|
+
const diff = createListDiff(listParentListIndex, oldList, merge.list);
|
|
11683
|
+
const result = setByAddressCore(target, address, merge.list, receiver, handler, listPath);
|
|
11684
|
+
const elementPathInfo = getPathInfo(listPath + DELIMITER + WILDCARD);
|
|
11685
|
+
for (const match of merge.matched) {
|
|
11686
|
+
const fieldWrites = collectFieldWrites(match.oldRow, match.newRow);
|
|
11687
|
+
if (fieldWrites.length === 0) {
|
|
11688
|
+
continue;
|
|
11689
|
+
}
|
|
11690
|
+
// createListDiff の契約上 newIndexes の長さはハイブリッド配列と一致するため
|
|
11691
|
+
// 通常 undefined にはならない。仮に不変条件が破れても、per-path 書き込みを
|
|
11692
|
+
// 諦めるだけで値そのものは行オブジェクトへ反映する(skip すると state だけが
|
|
11693
|
+
// 旧値のまま残り、本機能が塞ごうとしている stale を作ってしまう)。
|
|
11694
|
+
const listIndex = diff.newIndexes[match.position];
|
|
11695
|
+
for (const write of fieldWrites) {
|
|
11696
|
+
if (typeof listIndex === "undefined") {
|
|
11697
|
+
match.oldRow[write.field] = write.value;
|
|
11698
|
+
continue;
|
|
11699
|
+
}
|
|
11700
|
+
const fieldPathInfo = getPathInfo(elementPathInfo.path + DELIMITER + write.field);
|
|
11701
|
+
const fieldAddress = createStateAddress(fieldPathInfo, listIndex);
|
|
11702
|
+
setByAddress(target, fieldAddress, write.value, receiver, handler);
|
|
11703
|
+
}
|
|
11704
|
+
}
|
|
11705
|
+
return result;
|
|
11706
|
+
}
|
|
10374
11707
|
function setByAddress(target, address, value, receiver, handler) {
|
|
11708
|
+
const listKeys = handler.stateElement.listKeys;
|
|
11709
|
+
if (listKeys != null && Array.isArray(value)) {
|
|
11710
|
+
const keySpec = listKeys.get(address.pathInfo.path);
|
|
11711
|
+
if (typeof keySpec !== "undefined") {
|
|
11712
|
+
const oldValue = getByAddress(target, address, receiver, handler);
|
|
11713
|
+
const merge = mergeKeyedList(address.pathInfo.path, keySpec, oldValue, value);
|
|
11714
|
+
if (merge !== null) {
|
|
11715
|
+
// merge が非 null なのは oldValue が非空配列のときだけ(mergeKeyedList 参照)
|
|
11716
|
+
return setKeyedListByAddress(target, address, merge, oldValue, receiver, handler);
|
|
11717
|
+
}
|
|
11718
|
+
}
|
|
11719
|
+
}
|
|
11720
|
+
return setByAddressCore(target, address, value, receiver, handler, null);
|
|
11721
|
+
}
|
|
11722
|
+
function setByAddressCore(target, address, value, receiver, handler, keyedMergePath) {
|
|
10375
11723
|
const stateElement = handler.stateElement;
|
|
10376
11724
|
const path = address.pathInfo.path;
|
|
10377
11725
|
// occurrence(wc-bindable の `semantics: "event"`)由来の書き込みは、同値でも
|
|
@@ -10404,8 +11752,7 @@ function setByAddress(target, address, value, receiver, handler) {
|
|
|
10404
11752
|
devOldValue = oldValue;
|
|
10405
11753
|
devHasOldValue = true;
|
|
10406
11754
|
}
|
|
10407
|
-
const cacheable = address
|
|
10408
|
-
stateElement.getterPaths.has(path);
|
|
11755
|
+
const cacheable = isCacheable(stateElement, address);
|
|
10409
11756
|
const absPathInfo = getAbsolutePathInfo(stateElement, address.pathInfo);
|
|
10410
11757
|
const absAddress = createAbsoluteStateAddress(absPathInfo, address.listIndex);
|
|
10411
11758
|
if (devtoolsSink !== null) {
|
|
@@ -10424,24 +11771,15 @@ function setByAddress(target, address, value, receiver, handler) {
|
|
|
10424
11771
|
return Reflect.set(parentValue, key, value);
|
|
10425
11772
|
}
|
|
10426
11773
|
finally {
|
|
10427
|
-
notifyWrite(address, absAddress, receiver, handler);
|
|
11774
|
+
notifyWrite(address, absAddress, receiver, handler, keyedMergePath);
|
|
10428
11775
|
if (cacheable) {
|
|
10429
11776
|
setCacheEntryByAbsoluteStateAddress(absAddress, {
|
|
10430
11777
|
value: value,
|
|
10431
11778
|
dirty: false
|
|
10432
11779
|
});
|
|
10433
11780
|
}
|
|
10434
|
-
// DCC bindable
|
|
10435
|
-
|
|
10436
|
-
if (eventName) {
|
|
10437
|
-
const rootNode = stateElement.rootNode;
|
|
10438
|
-
if (rootNode instanceof ShadowRoot) {
|
|
10439
|
-
rootNode.host.dispatchEvent(new CustomEvent(eventName, {
|
|
10440
|
-
detail: value,
|
|
10441
|
-
bubbles: true,
|
|
10442
|
-
}));
|
|
10443
|
-
}
|
|
10444
|
-
}
|
|
11781
|
+
// DCC bindable イベントディスパッチ(完全一致 + サブパス → 先頭セグメント、§2.1)
|
|
11782
|
+
dispatchBindableEvent(stateElement, address.pathInfo, { value });
|
|
10445
11783
|
}
|
|
10446
11784
|
}
|
|
10447
11785
|
}
|
|
@@ -10464,8 +11802,7 @@ function setByAddress(target, address, value, receiver, handler) {
|
|
|
10464
11802
|
}
|
|
10465
11803
|
// --- end same-value guard ---
|
|
10466
11804
|
const isSwappable = stateElement.elementPaths.has(address.pathInfo.path);
|
|
10467
|
-
const cacheable = address
|
|
10468
|
-
stateElement.getterPaths.has(address.pathInfo.path);
|
|
11805
|
+
const cacheable = isCacheable(stateElement, address);
|
|
10469
11806
|
const absPathInfo = getAbsolutePathInfo(stateElement, address.pathInfo);
|
|
10470
11807
|
const absAddress = createAbsoluteStateAddress(absPathInfo, address.listIndex);
|
|
10471
11808
|
if (devtoolsSink !== null) {
|
|
@@ -10479,10 +11816,10 @@ function setByAddress(target, address, value, receiver, handler) {
|
|
|
10479
11816
|
}
|
|
10480
11817
|
try {
|
|
10481
11818
|
if (isSwappable) {
|
|
10482
|
-
return _setByAddressWithSwap(target, address, absAddress, value, receiver, handler);
|
|
11819
|
+
return _setByAddressWithSwap(target, address, absAddress, value, receiver, handler, keyedMergePath);
|
|
10483
11820
|
}
|
|
10484
11821
|
else {
|
|
10485
|
-
return _setByAddress(target, address, absAddress, value, receiver, handler);
|
|
11822
|
+
return _setByAddress(target, address, absAddress, value, receiver, handler, keyedMergePath);
|
|
10486
11823
|
}
|
|
10487
11824
|
}
|
|
10488
11825
|
finally {
|
|
@@ -10492,17 +11829,8 @@ function setByAddress(target, address, value, receiver, handler) {
|
|
|
10492
11829
|
dirty: false
|
|
10493
11830
|
});
|
|
10494
11831
|
}
|
|
10495
|
-
// DCC bindable
|
|
10496
|
-
|
|
10497
|
-
if (eventName) {
|
|
10498
|
-
const rootNode = stateElement.rootNode;
|
|
10499
|
-
if (rootNode instanceof ShadowRoot) {
|
|
10500
|
-
rootNode.host.dispatchEvent(new CustomEvent(eventName, {
|
|
10501
|
-
detail: value,
|
|
10502
|
-
bubbles: true,
|
|
10503
|
-
}));
|
|
10504
|
-
}
|
|
10505
|
-
}
|
|
11832
|
+
// DCC bindable イベントディスパッチ(完全一致 + サブパス → 先頭セグメント、§2.1)
|
|
11833
|
+
dispatchBindableEvent(stateElement, address.pathInfo, { value });
|
|
10506
11834
|
}
|
|
10507
11835
|
}
|
|
10508
11836
|
|
|
@@ -10550,8 +11878,10 @@ function resolve(target, _prop, receiver, handler) {
|
|
|
10550
11878
|
raiseError(`ListIndexes not found: ${wildcardParentPathInfo.path}`);
|
|
10551
11879
|
}
|
|
10552
11880
|
const index = indexes[i];
|
|
11881
|
+
// 範囲外 index はリスト自体の不在と別原因なので index を含める
|
|
11882
|
+
// (docs/state-bind-component-nested-for-design.md §8.4)
|
|
10553
11883
|
listIndex = listIndexes[index] ??
|
|
10554
|
-
raiseError(`ListIndex not found
|
|
11884
|
+
raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
|
|
10555
11885
|
}
|
|
10556
11886
|
// ToDo:WritableかReadonlyかを判定して適切なメソッドを呼び出す
|
|
10557
11887
|
const address = createStateAddress(pathInfo, listIndex);
|
|
@@ -10593,7 +11923,7 @@ function getAll(target, prop, receiver, handler) {
|
|
|
10593
11923
|
const wildcardPattern = pathInfo.wildcardParentPathInfos[i];
|
|
10594
11924
|
const listIndex = getContextListIndex(handler, wildcardPattern.path);
|
|
10595
11925
|
if (listIndex) {
|
|
10596
|
-
indexes = listIndex.
|
|
11926
|
+
indexes = getScopedIndexes(listIndex, listIndex.length - getBaseDepth(handler.stateElement));
|
|
10597
11927
|
break;
|
|
10598
11928
|
}
|
|
10599
11929
|
}
|
|
@@ -10610,7 +11940,7 @@ function getAll(target, prop, receiver, handler) {
|
|
|
10610
11940
|
const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
|
|
10611
11941
|
const oldValue = lastValueByListAddress.get(wildcardAddress);
|
|
10612
11942
|
const newValue = getByAddress(target, wildcardAddress, receiver, handler);
|
|
10613
|
-
const listDiff = createListDiff(listIndex, oldValue, newValue);
|
|
11943
|
+
const listDiff = createListDiff(getListParentListIndex(handler.stateElement, listIndex), oldValue, newValue);
|
|
10614
11944
|
const listIndexes = listDiff.newIndexes;
|
|
10615
11945
|
const index = indexes[indexPos] ?? null;
|
|
10616
11946
|
newValueByAddress.set(wildcardAddress, newValue);
|
|
@@ -10621,8 +11951,10 @@ function getAll(target, prop, receiver, handler) {
|
|
|
10621
11951
|
}
|
|
10622
11952
|
}
|
|
10623
11953
|
else {
|
|
11954
|
+
// 範囲外 index はリスト自体の不在と別原因なので index を含める
|
|
11955
|
+
// (docs/state-bind-component-nested-for-design.md §8.4)
|
|
10624
11956
|
const listIndex = listIndexes[index] ??
|
|
10625
|
-
raiseError(`ListIndex not found
|
|
11957
|
+
raiseError(`ListIndex not found at index ${index} of ${wildcardParentPathInfo.path}`);
|
|
10626
11958
|
if ((wildcardIndexPos + 1) < wildcardParentPathInfos.length) {
|
|
10627
11959
|
walkWildcardPattern(wildcardParentPathInfos, wildcardIndexPos + 1, listIndex, indexes, indexPos + 1, parentIndexes.concat(listIndex.index), results);
|
|
10628
11960
|
}
|
|
@@ -10684,8 +12016,11 @@ function getListIndex(target, resolvedAddress, receiver, handler) {
|
|
|
10684
12016
|
raiseError(`ListIndex not found: ${wildcardParentPathInfo.path}`);
|
|
10685
12017
|
const wildcardIndex = resolvedAddress.wildcardIndexes[i] ??
|
|
10686
12018
|
raiseError(`wildcardIndex is null: ${resolvedAddress.pathInfo.path}`);
|
|
12019
|
+
// 範囲外 index はリスト自体の不在と別原因なので、メッセージに index を含める。
|
|
12020
|
+
// 親パスだけを名指しすると「リスト自体が見つからない」と誤読させる
|
|
12021
|
+
// (docs/state-bind-component-nested-for-design.md §8.4)。
|
|
10687
12022
|
parentListIndex = wildcardParentListIndexes[wildcardIndex] ??
|
|
10688
|
-
raiseError(`ListIndex not found
|
|
12023
|
+
raiseError(`ListIndex not found at index ${wildcardIndex} of ${wildcardParentPathInfo.path}`);
|
|
10689
12024
|
}
|
|
10690
12025
|
return parentListIndex;
|
|
10691
12026
|
}
|
|
@@ -10714,6 +12049,10 @@ function postUpdate(target, _prop, receiver, handler) {
|
|
|
10714
12049
|
// 更新対象として登録
|
|
10715
12050
|
updater.enqueueAbsoluteAddress(absDepAddress);
|
|
10716
12051
|
});
|
|
12052
|
+
// DCC bindable イベントディスパッチ。$postUpdate は in-place 変異を通知する正規の idiom で、
|
|
12053
|
+
// set トラップを通らない変更が観測面に出る唯一の経路なので、ここでも撃つ
|
|
12054
|
+
// (docs/architecture-hardening/15-state-component-mechanism-consistency.md §2.1)。
|
|
12055
|
+
dispatchBindableEvent(stateElement, address.pathInfo);
|
|
10717
12056
|
};
|
|
10718
12057
|
}
|
|
10719
12058
|
|
|
@@ -10818,7 +12157,7 @@ function updatedCallback(target, refs, receiver, handler) {
|
|
|
10818
12157
|
}
|
|
10819
12158
|
paths.add(pathName);
|
|
10820
12159
|
if (pathInfo.wildcardCount > 0) {
|
|
10821
|
-
const indexes = ref.listIndex.
|
|
12160
|
+
const indexes = getScopedIndexes(ref.listIndex, pathInfo.wildcardCount);
|
|
10822
12161
|
const indexesList = indexesListByPath[pathName];
|
|
10823
12162
|
if (typeof indexesList === "undefined") {
|
|
10824
12163
|
indexesListByPath[pathName] = [indexes];
|
|
@@ -10935,7 +12274,13 @@ function get(target, prop, receiver, handler) {
|
|
|
10935
12274
|
handler.stateElement.addIndexDependentGetterPath?.(lastInfo.path);
|
|
10936
12275
|
}
|
|
10937
12276
|
const listIndex = lastAddress?.listIndex;
|
|
10938
|
-
|
|
12277
|
+
if (typeof listIndex === "undefined" || listIndex === null) {
|
|
12278
|
+
raiseError(`ListIndex not found: ${prop.toString()}`);
|
|
12279
|
+
}
|
|
12280
|
+
// `$1` は「このスコープの」1 段目。base 深さ Δ を持つ子スコープでも
|
|
12281
|
+
// 番号がずれないよう末尾から数える(list/wildcardLevel.ts)
|
|
12282
|
+
const indexListIndex = listIndexAtWildcard(listIndex, index, lastAddress.pathInfo.wildcardCount);
|
|
12283
|
+
return indexListIndex?.index ?? raiseError(`ListIndex not found: ${prop.toString()}`);
|
|
10939
12284
|
}
|
|
10940
12285
|
if (typeof prop === "string") {
|
|
10941
12286
|
if (prop[0] === '$') {
|
|
@@ -11172,138 +12517,6 @@ function createStateProxy(rootNode, state, stateName, mutability) {
|
|
|
11172
12517
|
return stateProxy;
|
|
11173
12518
|
}
|
|
11174
12519
|
|
|
11175
|
-
// WebComponent専用のキャッシュ
|
|
11176
|
-
// outerState.tsからのアクセスで、これを返す
|
|
11177
|
-
const lastValueByAbsoluteStateAddress = new WeakMap();
|
|
11178
|
-
function setLastValueByAbsoluteStateAddress(absoluteStateAddress, value) {
|
|
11179
|
-
lastValueByAbsoluteStateAddress.set(absoluteStateAddress, value);
|
|
11180
|
-
}
|
|
11181
|
-
function getLastValueByAbsoluteStateAddress(absoluteStateAddress) {
|
|
11182
|
-
return lastValueByAbsoluteStateAddress.get(absoluteStateAddress);
|
|
11183
|
-
}
|
|
11184
|
-
|
|
11185
|
-
const stateElementByWebComponent = new WeakMap();
|
|
11186
|
-
function setStateElementByWebComponent(webComponent, stateName, stateElement) {
|
|
11187
|
-
let stateMap = stateElementByWebComponent.get(webComponent);
|
|
11188
|
-
if (!stateMap) {
|
|
11189
|
-
stateMap = new Map();
|
|
11190
|
-
stateElementByWebComponent.set(webComponent, stateMap);
|
|
11191
|
-
}
|
|
11192
|
-
stateMap.set(stateName, stateElement);
|
|
11193
|
-
}
|
|
11194
|
-
function getStateElementByWebComponent(webComponent, stateName) {
|
|
11195
|
-
const stateMap = stateElementByWebComponent.get(webComponent);
|
|
11196
|
-
if (!stateMap) {
|
|
11197
|
-
return null;
|
|
11198
|
-
}
|
|
11199
|
-
return stateMap.get(stateName) ?? null;
|
|
11200
|
-
}
|
|
11201
|
-
|
|
11202
|
-
const innerMappingByElement = new WeakMap();
|
|
11203
|
-
const outerMappingByElement = new WeakMap();
|
|
11204
|
-
const primaryMappingRuleSetByElement = new WeakMap();
|
|
11205
|
-
const primaryBindingByMappingRule = new WeakMap();
|
|
11206
|
-
function createMappingRuleByBinding(innerState, binding) {
|
|
11207
|
-
const innerPathInfo = getPathInfo(binding.propSegments.slice(1).join(DELIMITER));
|
|
11208
|
-
const innerAbsPathInfo = getAbsolutePathInfo(innerState, innerPathInfo);
|
|
11209
|
-
const outerAbsStateAddress = getAbsoluteStateAddressByBinding(binding);
|
|
11210
|
-
const outerAbsPathInfo = outerAbsStateAddress.absolutePathInfo;
|
|
11211
|
-
return { innerAbsPathInfo, outerAbsPathInfo };
|
|
11212
|
-
}
|
|
11213
|
-
function buildPrimaryMappingRule(webComponent, stateName, bindings) {
|
|
11214
|
-
if (bindings.length === 0) {
|
|
11215
|
-
return;
|
|
11216
|
-
}
|
|
11217
|
-
const innerState = getStateElementByWebComponent(webComponent, stateName);
|
|
11218
|
-
if (innerState === null) {
|
|
11219
|
-
raiseError('State element not found for web component.');
|
|
11220
|
-
}
|
|
11221
|
-
const innerMappingRule = new Map();
|
|
11222
|
-
const outerMappingRule = new Map();
|
|
11223
|
-
for (const binding of bindings) {
|
|
11224
|
-
const mappingRule = createMappingRuleByBinding(innerState, binding);
|
|
11225
|
-
let primaryMappingRuleSet = primaryMappingRuleSetByElement.get(webComponent);
|
|
11226
|
-
if (typeof primaryMappingRuleSet === 'undefined') {
|
|
11227
|
-
primaryMappingRuleSetByElement.set(webComponent, new Set([mappingRule]));
|
|
11228
|
-
}
|
|
11229
|
-
else {
|
|
11230
|
-
primaryMappingRuleSet.add(mappingRule);
|
|
11231
|
-
}
|
|
11232
|
-
const innerAbsPathInfo = mappingRule.innerAbsPathInfo;
|
|
11233
|
-
const outerAbsPathInfo = mappingRule.outerAbsPathInfo;
|
|
11234
|
-
primaryBindingByMappingRule.set(mappingRule, binding);
|
|
11235
|
-
innerMappingRule.set(innerAbsPathInfo, outerAbsPathInfo);
|
|
11236
|
-
outerMappingRule.set(outerAbsPathInfo, innerAbsPathInfo);
|
|
11237
|
-
}
|
|
11238
|
-
innerMappingByElement.set(webComponent, innerMappingRule);
|
|
11239
|
-
outerMappingByElement.set(webComponent, outerMappingRule);
|
|
11240
|
-
}
|
|
11241
|
-
function getOuterAbsolutePathInfo(webComponent, innerAbsPathInfo) {
|
|
11242
|
-
let innerMapping = innerMappingByElement.get(webComponent);
|
|
11243
|
-
if (typeof innerMapping === 'undefined') {
|
|
11244
|
-
innerMapping = new Map();
|
|
11245
|
-
innerMappingByElement.set(webComponent, innerMapping);
|
|
11246
|
-
}
|
|
11247
|
-
if (innerMapping.has(innerAbsPathInfo)) {
|
|
11248
|
-
return innerMapping.get(innerAbsPathInfo);
|
|
11249
|
-
}
|
|
11250
|
-
let outerMapping = outerMappingByElement.get(webComponent);
|
|
11251
|
-
if (typeof outerMapping === 'undefined') {
|
|
11252
|
-
outerMapping = new Map();
|
|
11253
|
-
outerMappingByElement.set(webComponent, outerMapping);
|
|
11254
|
-
}
|
|
11255
|
-
// 内側からのアクセスの場合、ルールがなければプライマリルールから新たにルールとバインディングを生成する
|
|
11256
|
-
const primaryMappingRuleSet = primaryMappingRuleSetByElement.get(webComponent);
|
|
11257
|
-
if (typeof primaryMappingRuleSet === 'undefined') {
|
|
11258
|
-
// マッピングルールが存在しない場合はnullを返し、ローカル状態へのフォールバックを許可する
|
|
11259
|
-
return null;
|
|
11260
|
-
}
|
|
11261
|
-
let primaryMappingRule = null;
|
|
11262
|
-
for (const currentPrimaryMappingRule of primaryMappingRuleSet) {
|
|
11263
|
-
// innerPathInfoがprimaryMappingRuleのinnerPathInfoを包含しているか
|
|
11264
|
-
if (!innerAbsPathInfo.pathInfo.cumulativePathInfoSet.has(currentPrimaryMappingRule.innerAbsPathInfo.pathInfo)) {
|
|
11265
|
-
continue;
|
|
11266
|
-
}
|
|
11267
|
-
if (currentPrimaryMappingRule.innerAbsPathInfo.pathInfo.segments.length === innerAbsPathInfo.pathInfo.segments.length) {
|
|
11268
|
-
raiseError('Duplicate mapping rule for web component.');
|
|
11269
|
-
}
|
|
11270
|
-
primaryMappingRule = currentPrimaryMappingRule;
|
|
11271
|
-
break;
|
|
11272
|
-
}
|
|
11273
|
-
if (primaryMappingRule === null) {
|
|
11274
|
-
// マッピングルールに一致しない場合はnullを返し、ローカル状態へのフォールバックを許可する
|
|
11275
|
-
return null;
|
|
11276
|
-
}
|
|
11277
|
-
// マッチした残りのパスをouterPathInfoに付与して新たなルールを生成
|
|
11278
|
-
const primaryBinding = primaryBindingByMappingRule.get(primaryMappingRule);
|
|
11279
|
-
/* c8 ignore start */
|
|
11280
|
-
if (typeof primaryBinding === 'undefined') {
|
|
11281
|
-
raiseError('Binding not found for primary mapping rule on web component.');
|
|
11282
|
-
}
|
|
11283
|
-
/* c8 ignore stop */
|
|
11284
|
-
const outerRemainingSegments = innerAbsPathInfo.pathInfo.segments.slice(primaryMappingRule.innerAbsPathInfo.pathInfo.segments.length);
|
|
11285
|
-
const outerSegments = primaryMappingRule.outerAbsPathInfo.pathInfo.segments.concat(outerRemainingSegments);
|
|
11286
|
-
const outerPathInfo = getPathInfo(outerSegments.join(DELIMITER));
|
|
11287
|
-
const rootNode = webComponent.getRootNode();
|
|
11288
|
-
const outerStateElement = getStateElementByName(rootNode, primaryBinding.stateName);
|
|
11289
|
-
if (outerStateElement === null) {
|
|
11290
|
-
raiseError(`State element with name "${primaryBinding.stateName}" not found for web component.`);
|
|
11291
|
-
}
|
|
11292
|
-
const outerAbsPathInfo = getAbsolutePathInfo(outerStateElement, outerPathInfo);
|
|
11293
|
-
innerMapping.set(innerAbsPathInfo, outerAbsPathInfo);
|
|
11294
|
-
outerMapping.set(outerAbsPathInfo, innerAbsPathInfo);
|
|
11295
|
-
// ルールに対応するバインディングを生成
|
|
11296
|
-
const newBinding = {
|
|
11297
|
-
...primaryBinding,
|
|
11298
|
-
propName: innerAbsPathInfo.pathInfo.path,
|
|
11299
|
-
propSegments: innerAbsPathInfo.pathInfo.segments,
|
|
11300
|
-
statePathName: outerAbsPathInfo.pathInfo.path,
|
|
11301
|
-
statePathInfo: outerAbsPathInfo.pathInfo,
|
|
11302
|
-
};
|
|
11303
|
-
addBindingByNode(webComponent, newBinding);
|
|
11304
|
-
return outerAbsPathInfo;
|
|
11305
|
-
}
|
|
11306
|
-
|
|
11307
12520
|
function cloneWithDescriptors(obj) {
|
|
11308
12521
|
const proto = Object.getPrototypeOf(obj);
|
|
11309
12522
|
const clone = Object.create(proto);
|
|
@@ -11328,6 +12541,39 @@ class InnerStateProxyHandler {
|
|
|
11328
12541
|
this._webComponent = webComponent;
|
|
11329
12542
|
this._innerStateElement = getStateElementByWebComponent(webComponent, stateName) ?? raiseError('State element not found for web component.');
|
|
11330
12543
|
}
|
|
12544
|
+
/**
|
|
12545
|
+
* 親スコープで読み書きするときのループ文脈を決める。候補は 2 つある。
|
|
12546
|
+
*
|
|
12547
|
+
* 1. **越境直前のアドレスの listIndex**。子スコープの `for` が回している行
|
|
12548
|
+
* (§1.8)。子の listIndex は base(=ホストの親スコープ行)を親に持つので
|
|
12549
|
+
* チェーン長は Δ+W_inner = W_outer になり、そのまま外側の文脈として使える
|
|
12550
|
+
* (docs/state-bind-component-nested-for-design.md)。
|
|
12551
|
+
* 2. **コンポーネント要素のノードループ文脈**。コンポーネント自身が親の `for` の
|
|
12552
|
+
* 中にいるが、読んでいるパスは子スコープのループの外という形(`state.row: rows.*`)。
|
|
12553
|
+
*
|
|
12554
|
+
* 1 を先に見るのは、内側ほど具体的だから。入れ子形では 2 も非 null(=Δ 段だけ)に
|
|
12555
|
+
* なるが、それでは外側パスの段数に足りない。段数が一致する候補だけを採るのが
|
|
12556
|
+
* 判定の本体で、両方外れたら null(親側の解決に委ね、解けなければ raiseError。
|
|
12557
|
+
* 無言の取り違えを作らない)。
|
|
12558
|
+
*/
|
|
12559
|
+
_outerLoopContext(innerPathInfo, outerAbsPathInfo) {
|
|
12560
|
+
const outerWildcardCount = outerAbsPathInfo.pathInfo.wildcardCount;
|
|
12561
|
+
const nodeLoopContext = getLoopContextByNode(this._webComponent);
|
|
12562
|
+
if (nodeLoopContext !== null && nodeLoopContext.listIndex.length === outerWildcardCount) {
|
|
12563
|
+
return nodeLoopContext;
|
|
12564
|
+
}
|
|
12565
|
+
if (outerWildcardCount > 0) {
|
|
12566
|
+
const address = getCrossBoundaryAddress(this._innerStateElement, innerPathInfo.path);
|
|
12567
|
+
const listIndex = address?.listIndex ?? null;
|
|
12568
|
+
if (listIndex !== null && listIndex.length === outerWildcardCount) {
|
|
12569
|
+
const outerWildcardPath = outerAbsPathInfo.pathInfo.wildcardPaths[outerWildcardCount - 1];
|
|
12570
|
+
return createStateAddress(getPathInfo(outerWildcardPath), listIndex);
|
|
12571
|
+
}
|
|
12572
|
+
}
|
|
12573
|
+
// どちらも段数が合わない。従来どおりノードの文脈へフォールバックし、
|
|
12574
|
+
// 解けなければ後段が raiseError する(無言の取り違えを作らない)
|
|
12575
|
+
return nodeLoopContext;
|
|
12576
|
+
}
|
|
11331
12577
|
get(target, prop, receiver) {
|
|
11332
12578
|
if (typeof prop === 'string') {
|
|
11333
12579
|
if (prop === "then") {
|
|
@@ -11346,21 +12592,11 @@ class InnerStateProxyHandler {
|
|
|
11346
12592
|
const innerAbsPathInfo = getAbsolutePathInfo(this._innerStateElement, innerPathInfo);
|
|
11347
12593
|
const outerAbsPathInfo = getOuterAbsolutePathInfo(this._webComponent, innerAbsPathInfo);
|
|
11348
12594
|
if (outerAbsPathInfo !== null) {
|
|
11349
|
-
const loopContext =
|
|
12595
|
+
const loopContext = this._outerLoopContext(innerPathInfo, outerAbsPathInfo);
|
|
11350
12596
|
let value = undefined;
|
|
11351
12597
|
outerAbsPathInfo.stateElement.createState("readonly", (state) => {
|
|
11352
12598
|
state[setLoopContextSymbol](loopContext, () => {
|
|
11353
12599
|
value = state[outerAbsPathInfo.pathInfo.path];
|
|
11354
|
-
let listIndex = null;
|
|
11355
|
-
if (loopContext !== null && loopContext.listIndex !== null) {
|
|
11356
|
-
if (outerAbsPathInfo.pathInfo.wildcardCount > 0) {
|
|
11357
|
-
// wildcardPathSetとloopContextのpathInfoSetのintersectionのうち、segment数が最も多いものをouterAbsPathInfoにする
|
|
11358
|
-
// 例: outerPathInfoが "todos.*.name"で、loopContextのpathInfoSetに "todos.0.name", "todos.1.name"がある場合、"todos.0.name"や"todos.1.name"をouterAbsPathInfoにする
|
|
11359
|
-
listIndex = loopContext.listIndex.at(outerAbsPathInfo.pathInfo.wildcardCount - 1);
|
|
11360
|
-
}
|
|
11361
|
-
}
|
|
11362
|
-
const absStateAddress = createAbsoluteStateAddress(outerAbsPathInfo, listIndex);
|
|
11363
|
-
setLastValueByAbsoluteStateAddress(absStateAddress, value);
|
|
11364
12600
|
});
|
|
11365
12601
|
});
|
|
11366
12602
|
return value;
|
|
@@ -11387,7 +12623,7 @@ class InnerStateProxyHandler {
|
|
|
11387
12623
|
const innerAbsPathInfo = getAbsolutePathInfo(this._innerStateElement, innerPathInfo);
|
|
11388
12624
|
const outerAbsPathInfo = getOuterAbsolutePathInfo(this._webComponent, innerAbsPathInfo);
|
|
11389
12625
|
if (outerAbsPathInfo !== null) {
|
|
11390
|
-
const loopContext =
|
|
12626
|
+
const loopContext = this._outerLoopContext(innerPathInfo, outerAbsPathInfo);
|
|
11391
12627
|
outerAbsPathInfo.stateElement.createState("writable", (state) => {
|
|
11392
12628
|
state[setLoopContextSymbol](loopContext, () => {
|
|
11393
12629
|
state[outerAbsPathInfo.pathInfo.path] = value;
|
|
@@ -11455,42 +12691,24 @@ function createInnerState(webComponent, stateName) {
|
|
|
11455
12691
|
return new Proxy(meltFrozenObject(state), handler);
|
|
11456
12692
|
}
|
|
11457
12693
|
|
|
12694
|
+
/**
|
|
12695
|
+
* コンポーネントの `bind-component` プロパティとして露出する proxy。
|
|
12696
|
+
* read / write とも子の state proxy へ素通しする。
|
|
12697
|
+
*
|
|
12698
|
+
* mapped(親から `<prop>.*` をバインドされている)ケースでは、素通し先の
|
|
12699
|
+
* innerState proxy がマッピング規則に従って親 state へ解決するので、
|
|
12700
|
+
* `this.state.msg` の読みは親の現在値になり、書きは親 state へ届く。
|
|
12701
|
+
* plain(親からのバインドなし)ケースでは子のローカル state に解決する。
|
|
12702
|
+
* **どちらでも同じ意味論になる**のが要点
|
|
12703
|
+
* (docs/architecture-hardening/15-state-component-mechanism-consistency.md §1.1 / G1)。
|
|
12704
|
+
*
|
|
12705
|
+
* 以前は mapped 専用に「read = 最後に観測した値のキャッシュ/write = 値を捨てて
|
|
12706
|
+
* `$postUpdate` 通知のみ」という別 proxy を当てていた。あれは親 → 子の再読込通知という
|
|
12707
|
+
* **内部チャネル**としては正しかったが、それが公開 API を兼ねていたため、同じ
|
|
12708
|
+
* コンポーネント実装が親ページの書き方で挙動を変えていた。内部チャネルは
|
|
12709
|
+
* `applyChangeToWebComponent` が state element を直接引く形へ分離した。
|
|
12710
|
+
*/
|
|
11458
12711
|
class OuterStateProxyHandler {
|
|
11459
|
-
_innerStateElement;
|
|
11460
|
-
constructor(webComponent, stateName) {
|
|
11461
|
-
this._innerStateElement = getStateElementByWebComponent(webComponent, stateName) ?? raiseError('State element not found for web component.');
|
|
11462
|
-
}
|
|
11463
|
-
get(target, prop, receiver) {
|
|
11464
|
-
if (typeof prop === 'string') {
|
|
11465
|
-
const innerPathInfo = getPathInfo(prop);
|
|
11466
|
-
const innerAbsPathInfo = getAbsolutePathInfo(this._innerStateElement, innerPathInfo);
|
|
11467
|
-
const absStateAddress = createAbsoluteStateAddress(innerAbsPathInfo, null);
|
|
11468
|
-
return getLastValueByAbsoluteStateAddress(absStateAddress);
|
|
11469
|
-
}
|
|
11470
|
-
else {
|
|
11471
|
-
return Reflect.get(target, prop, receiver);
|
|
11472
|
-
}
|
|
11473
|
-
}
|
|
11474
|
-
set(target, prop, value, receiver) {
|
|
11475
|
-
if (typeof prop === 'string') {
|
|
11476
|
-
const innerPathInfo = getPathInfo(prop);
|
|
11477
|
-
const innerAbsPathInfo = getAbsolutePathInfo(this._innerStateElement, innerPathInfo);
|
|
11478
|
-
this._innerStateElement.createState("readonly", (state) => {
|
|
11479
|
-
state.$postUpdate(innerAbsPathInfo.pathInfo.path);
|
|
11480
|
-
});
|
|
11481
|
-
return true;
|
|
11482
|
-
}
|
|
11483
|
-
else {
|
|
11484
|
-
return Reflect.set(target, prop, value, receiver);
|
|
11485
|
-
}
|
|
11486
|
-
}
|
|
11487
|
-
}
|
|
11488
|
-
function createOuterState(webComponent, stateName) {
|
|
11489
|
-
const handler = new OuterStateProxyHandler(webComponent, stateName);
|
|
11490
|
-
return new Proxy({}, handler);
|
|
11491
|
-
}
|
|
11492
|
-
|
|
11493
|
-
class PlainOuterStateProxyHandler {
|
|
11494
12712
|
_innerStateElement;
|
|
11495
12713
|
constructor(webComponent, stateName) {
|
|
11496
12714
|
this._innerStateElement = getStateElementByWebComponent(webComponent, stateName) ?? raiseError('State element not found for web component.');
|
|
@@ -11519,36 +12737,44 @@ class PlainOuterStateProxyHandler {
|
|
|
11519
12737
|
}
|
|
11520
12738
|
}
|
|
11521
12739
|
}
|
|
11522
|
-
function
|
|
11523
|
-
const handler = new
|
|
12740
|
+
function createOuterState(webComponent, stateName) {
|
|
12741
|
+
const handler = new OuterStateProxyHandler(webComponent, stateName);
|
|
11524
12742
|
return new Proxy({}, handler);
|
|
11525
12743
|
}
|
|
11526
12744
|
|
|
11527
12745
|
const getOuter = (outerState) => () => outerState;
|
|
11528
12746
|
function bindWebComponent(innerStateElement, component, stateProp, state) {
|
|
11529
12747
|
setStateElementByWebComponent(component, stateProp, innerStateElement);
|
|
11530
|
-
|
|
11531
|
-
|
|
12748
|
+
// 分岐は「data-wcs 属性の有無」ではなく「<stateProp>.* バインドが 1 件以上あるか」で決める。
|
|
12749
|
+
// 属性はあってもマッピング対象が 0 件(例: data-wcs="class.on: flag" だけ)の場合、
|
|
12750
|
+
// buildPrimaryMappingRule は primaryMappingRule を 1 件も作らないまま return するため、
|
|
12751
|
+
// outerState の lastValue / $postUpdate 意味論だけが残る。その状態では
|
|
12752
|
+
// component[stateProp] の read が常に undefined・write が完全な no-op になる
|
|
12753
|
+
// (docs/architecture-hardening/15-state-component-mechanism-consistency.md §1.2)。
|
|
12754
|
+
const bindings = component.hasAttribute(config.bindAttributeName)
|
|
12755
|
+
? (getBindingsByNode(component) ?? []).filter(binding => binding.propSegments[0] === stateProp)
|
|
12756
|
+
: [];
|
|
12757
|
+
// 分岐が決めるのは「子の state の中身」だけ。mapped なら親 state へ解決する
|
|
12758
|
+
// innerState proxy、plain なら melt 済みのローカル state。
|
|
12759
|
+
if (bindings.length > 0) {
|
|
11532
12760
|
buildPrimaryMappingRule(component, stateProp, bindings);
|
|
11533
|
-
|
|
11534
|
-
|
|
11535
|
-
innerStateElement.
|
|
11536
|
-
|
|
11537
|
-
get: getOuter(outerState),
|
|
11538
|
-
enumerable: true,
|
|
11539
|
-
configurable: true,
|
|
11540
|
-
});
|
|
12761
|
+
// 値の正本が親スコープにあることを state 要素に記録する。越境アドレスの受け渡しと
|
|
12762
|
+
// リストパスの外向き伝播はこのフラグでのみ有効になる(§1.8)。
|
|
12763
|
+
innerStateElement.markComponentStateMapped?.();
|
|
12764
|
+
innerStateElement.setInitialState(createInnerState(component, stateProp));
|
|
11541
12765
|
}
|
|
11542
12766
|
else {
|
|
11543
12767
|
innerStateElement.setInitialState(meltFrozenObject(state));
|
|
11544
|
-
const outerState = createPlainOuterState(component, stateProp);
|
|
11545
|
-
Object.defineProperty(component, stateProp, {
|
|
11546
|
-
get: getOuter(outerState),
|
|
11547
|
-
enumerable: true,
|
|
11548
|
-
configurable: true,
|
|
11549
|
-
});
|
|
11550
12768
|
}
|
|
11551
|
-
|
|
12769
|
+
// 外向きに露出する proxy は両者で同一。mapped でも read はライブ・write は
|
|
12770
|
+
// innerState 経由で親 state に届く(§1.1 / G1)。
|
|
12771
|
+
const outerState = createOuterState(component, stateProp);
|
|
12772
|
+
Object.defineProperty(component, stateProp, {
|
|
12773
|
+
get: getOuter(outerState),
|
|
12774
|
+
enumerable: true,
|
|
12775
|
+
configurable: true,
|
|
12776
|
+
});
|
|
12777
|
+
markWebComponentAsComplete(component, stateProp);
|
|
11552
12778
|
if (WEBCOMPONENT_STATE_READY_CALLBACK_NAME in component) {
|
|
11553
12779
|
const func = component[WEBCOMPONENT_STATE_READY_CALLBACK_NAME];
|
|
11554
12780
|
if (typeof func === 'function') {
|
|
@@ -11562,15 +12788,6 @@ function bindWebComponent(innerStateElement, component, stateProp, state) {
|
|
|
11562
12788
|
}
|
|
11563
12789
|
}
|
|
11564
12790
|
|
|
11565
|
-
function getAllPropertyDescriptors(obj) {
|
|
11566
|
-
let descriptors = {};
|
|
11567
|
-
let proto = obj;
|
|
11568
|
-
while (proto && proto !== Object.prototype) {
|
|
11569
|
-
Object.assign(descriptors, Object.getOwnPropertyDescriptors(proto));
|
|
11570
|
-
proto = Object.getPrototypeOf(proto);
|
|
11571
|
-
}
|
|
11572
|
-
return descriptors;
|
|
11573
|
-
}
|
|
11574
12791
|
function getStateInfo(state) {
|
|
11575
12792
|
const getterPaths = new Set();
|
|
11576
12793
|
const setterPaths = new Set();
|
|
@@ -11606,15 +12823,17 @@ class State extends HTMLElementBase {
|
|
|
11606
12823
|
_resolveInitialize = null;
|
|
11607
12824
|
_connectedCallbackPromise;
|
|
11608
12825
|
_resolveConnectedCallback = null;
|
|
12826
|
+
_rejectConnectedCallback = null;
|
|
11609
12827
|
_loadingPromise;
|
|
11610
12828
|
_resolveLoading = null;
|
|
11611
12829
|
_setStatePromise = null;
|
|
11612
12830
|
_resolveSetState = null;
|
|
11613
12831
|
_listPaths = new Set();
|
|
12832
|
+
_listKeys = null;
|
|
11614
12833
|
_elementPaths = new Set();
|
|
11615
12834
|
_getterPaths = new Set();
|
|
11616
12835
|
_setterPaths = new Set();
|
|
11617
|
-
_loopContextStack = createLoopContextStack();
|
|
12836
|
+
_loopContextStack = createLoopContextStack(() => getBaseDepth(this));
|
|
11618
12837
|
_dynamicDependency = new Map();
|
|
11619
12838
|
_staticDependency = new Map();
|
|
11620
12839
|
_pathSet = new Set();
|
|
@@ -11622,6 +12841,7 @@ class State extends HTMLElementBase {
|
|
|
11622
12841
|
_rootNode = null;
|
|
11623
12842
|
_boundComponent = null;
|
|
11624
12843
|
_boundComponentStateProp = null;
|
|
12844
|
+
_hasMappedComponentState = false;
|
|
11625
12845
|
_bindableEventMap = {};
|
|
11626
12846
|
_commandTokenNames = new Set();
|
|
11627
12847
|
_eventTokenNames = new Set();
|
|
@@ -11641,8 +12861,9 @@ class State extends HTMLElementBase {
|
|
|
11641
12861
|
this._initializePromise = new Promise((resolve) => {
|
|
11642
12862
|
this._resolveInitialize = resolve;
|
|
11643
12863
|
});
|
|
11644
|
-
this._connectedCallbackPromise = new Promise((resolve) => {
|
|
12864
|
+
this._connectedCallbackPromise = new Promise((resolve, reject) => {
|
|
11645
12865
|
this._resolveConnectedCallback = resolve;
|
|
12866
|
+
this._rejectConnectedCallback = reject;
|
|
11646
12867
|
});
|
|
11647
12868
|
this._loadingPromise = new Promise((resolve) => {
|
|
11648
12869
|
this._resolveLoading = resolve;
|
|
@@ -11692,6 +12913,9 @@ class State extends HTMLElementBase {
|
|
|
11692
12913
|
clearStreamNamespace(this);
|
|
11693
12914
|
clearStreamRegistry(this);
|
|
11694
12915
|
processStreamsDeclaration(this, value);
|
|
12916
|
+
// $listKeys: 宣言が無ければ null のままで、setByAddress のキー突合経路には
|
|
12917
|
+
// 一切入らない(docs/state-list-key-design.md §7-1)。再 set で必ず置き換える。
|
|
12918
|
+
this._listKeys = processListKeysDeclaration(value);
|
|
11695
12919
|
// 接続中の再 set(S13)は新宣言で即再起動する。
|
|
11696
12920
|
// 初回(_initialize 中)は _initialized が false なのでここでは起動されず、
|
|
11697
12921
|
// connectedCallback 側の startStreams($connectedCallback 完了後)が担う。
|
|
@@ -11802,6 +13026,19 @@ class State extends HTMLElementBase {
|
|
|
11802
13026
|
if (!(parentNode instanceof ShadowRoot) && !this.hasAttribute("name")) {
|
|
11803
13027
|
raiseError(`"bind-component" in Light DOM requires a "name" attribute to avoid namespace conflicts with the parent scope.`);
|
|
11804
13028
|
}
|
|
13029
|
+
// bind-component はコンポーネント側の state プロパティを唯一のソースにする。
|
|
13030
|
+
// state / src / json / inner <script> と併記すると、この後の _initialize が
|
|
13031
|
+
// そちらを採用して _setStatePromise を await しないため、bindWebComponent が
|
|
13032
|
+
// setInitialState で渡した innerState proxy ごと捨てられ、親↔子マッピングが
|
|
13033
|
+
// 無言で死ぬ。併記は必ず設定ミスなので fail-fast させる
|
|
13034
|
+
// (docs/architecture-hardening/15-state-component-mechanism-consistency.md §2.6)。
|
|
13035
|
+
const conflicting = ["state", "src", "json"].filter((name) => this.hasAttribute(name));
|
|
13036
|
+
if (this.querySelector('script[type="module"]') !== null) {
|
|
13037
|
+
conflicting.push('<script type="module">');
|
|
13038
|
+
}
|
|
13039
|
+
if (conflicting.length > 0) {
|
|
13040
|
+
raiseError(`"bind-component" cannot be combined with ${conflicting.join(", ")}. The component's "${this.getAttribute("bind-component")}" property is the only state source.`);
|
|
13041
|
+
}
|
|
11805
13042
|
const boundComponentStateProp = this.getAttribute("bind-component");
|
|
11806
13043
|
await customElements.whenDefined(customTagName.toLowerCase());
|
|
11807
13044
|
// data-wcs属性がある場合は、上位の状態によりbinding情報の設定が完了するまで待機する
|
|
@@ -11820,6 +13057,34 @@ class State extends HTMLElementBase {
|
|
|
11820
13057
|
bindWebComponent(this, this._boundComponent, this._boundComponentStateProp, state);
|
|
11821
13058
|
}
|
|
11822
13059
|
}
|
|
13060
|
+
/**
|
|
13061
|
+
* mapped な `bind-component` が切断 → 再接続したときに、束ねているパスを読み直させる(§1.9)。
|
|
13062
|
+
*
|
|
13063
|
+
* リスト行の content は再利用されるので、行が作り直されると子はこの経路を通る
|
|
13064
|
+
* (`_initialized` が真なので `_initializeBindWebComponent` / `_initialize` は走らず、
|
|
13065
|
+
* 子のバインディングは張り直されない)。切断中に親で起きた変更の通知は
|
|
13066
|
+
* `applyChangeToWebComponent` が切断済みを理由に落としているため、ここで読み直さないと
|
|
13067
|
+
* 子のビューだけが古い値のまま取り残される。何が変わったかは分からないので、
|
|
13068
|
+
* プライマリ規則の粒度で丸ごと読み直す。
|
|
13069
|
+
*
|
|
13070
|
+
* 読み直しの前に派生規則の memo を捨てる。派生規則の購読者(親スコープに立つ
|
|
13071
|
+
* バインディング)は切断で teardown されており、memo が残っていると導出が二度と
|
|
13072
|
+
* 走らないため購読者も張り直されない = 以後この子だけがサブパスの書き込みを
|
|
13073
|
+
* 受け取れなくなる。捨てておけば、直後の読み直しで導出と購読者登録が走る。
|
|
13074
|
+
*/
|
|
13075
|
+
_reloadMappedPathsAfterReconnect() {
|
|
13076
|
+
if (!this._hasMappedComponentState || this._boundComponent === null) {
|
|
13077
|
+
return;
|
|
13078
|
+
}
|
|
13079
|
+
// mapped = プライマリ規則が 1 件以上あることと同義(bindWebComponent の分岐)
|
|
13080
|
+
const innerPaths = getPrimaryInnerPaths(this._boundComponent);
|
|
13081
|
+
resetDerivedMappingRules(this._boundComponent);
|
|
13082
|
+
this.createState("readonly", (state) => {
|
|
13083
|
+
for (const path of innerPaths) {
|
|
13084
|
+
state.$postUpdate(path);
|
|
13085
|
+
}
|
|
13086
|
+
});
|
|
13087
|
+
}
|
|
11823
13088
|
async _callStateConnectedCallback() {
|
|
11824
13089
|
await this.createStateAsync("writable", async (state) => {
|
|
11825
13090
|
// stateに"$connectedCallback"があるか確認し、connectedCallbackAPIを呼び出す
|
|
@@ -11880,6 +13145,13 @@ class State extends HTMLElementBase {
|
|
|
11880
13145
|
const parentNode = this.parentNode;
|
|
11881
13146
|
if (parentNode instanceof ShadowRoot &&
|
|
11882
13147
|
parentNode.host.hasAttribute(DCC_DEFINITION_ATTRIBUTE)) {
|
|
13148
|
+
// DCC と bind-component は排他。DCC の state はテンプレートに属し、
|
|
13149
|
+
// インスタンスごとにロードされるので、定義時点のホストのプロパティを
|
|
13150
|
+
// ソースにする bind-component とは両立しない。従来はこの return で
|
|
13151
|
+
// 無言に無視していた(docs/architecture-hardening/15 §3.1)。
|
|
13152
|
+
if (this.hasAttribute("bind-component")) {
|
|
13153
|
+
raiseError(`"bind-component" cannot be used inside a [${DCC_DEFINITION_ATTRIBUTE}] host. DCC state comes from the template, not from a component property.`);
|
|
13154
|
+
}
|
|
11883
13155
|
await this._initializeDCC(parentNode.host, parentNode);
|
|
11884
13156
|
return;
|
|
11885
13157
|
}
|
|
@@ -11893,6 +13165,7 @@ class State extends HTMLElementBase {
|
|
|
11893
13165
|
// createState が rootNode 経由でこの要素を解決できるようにするために必要
|
|
11894
13166
|
// ($connectedCallback の再実行と $streams の initial からの再起動が依存する、設計書 §2-3)。
|
|
11895
13167
|
setStateElementByName(this._rootNode, this._name, this);
|
|
13168
|
+
this._reloadMappedPathsAfterReconnect();
|
|
11896
13169
|
}
|
|
11897
13170
|
// enable-ssr (クライアント側): SSR で $connectedCallback 済みなのでスキップ
|
|
11898
13171
|
// inSsr() (サーバー側): レンダリング中なので実行する
|
|
@@ -11901,14 +13174,24 @@ class State extends HTMLElementBase {
|
|
|
11901
13174
|
}
|
|
11902
13175
|
// サーバーモード + enable-ssr: バインディング完了後に <wcs-ssr> を生成
|
|
11903
13176
|
if (inSsr() && this.hasAttribute('enable-ssr')) {
|
|
11904
|
-
|
|
11905
|
-
|
|
11906
|
-
|
|
11907
|
-
|
|
11908
|
-
|
|
11909
|
-
|
|
11910
|
-
|
|
11911
|
-
|
|
13177
|
+
try {
|
|
13178
|
+
await getBindingsReady(this.rootNode);
|
|
13179
|
+
const name = this.getAttribute('name') || 'default';
|
|
13180
|
+
const stateData = Ssr.extractStateData(this);
|
|
13181
|
+
const ssrEl = document.createElement(config.tagNames.ssr);
|
|
13182
|
+
ssrEl.setAttribute('name', name);
|
|
13183
|
+
ssrEl.setAttribute('version', VERSION);
|
|
13184
|
+
Ssr.buildContent(ssrEl, stateData);
|
|
13185
|
+
this.parentNode?.insertBefore(ssrEl, this);
|
|
13186
|
+
}
|
|
13187
|
+
catch (error) {
|
|
13188
|
+
// reject を配管しないと _connectedCallbackPromise が永久に未解決になり、
|
|
13189
|
+
// renderToString が mutex を握ったまま connectedCallbackPromise 待ちで
|
|
13190
|
+
// 無言ハングする。getBindingsReady の reject 化(設計書 §8.2)を
|
|
13191
|
+
// SSR の消費者(render.ts)まで届けるための対。
|
|
13192
|
+
this._rejectConnectedCallback?.(error);
|
|
13193
|
+
throw error;
|
|
13194
|
+
}
|
|
11912
13195
|
}
|
|
11913
13196
|
// $streams の eager 起動($connectedCallback 完了後、設計書 §2-3)。
|
|
11914
13197
|
// inSsr() 時は起動しない(SSR 出力には initial が乗る、§7-1)。
|
|
@@ -11959,6 +13242,9 @@ class State extends HTMLElementBase {
|
|
|
11959
13242
|
}
|
|
11960
13243
|
}
|
|
11961
13244
|
}
|
|
13245
|
+
get initialized() {
|
|
13246
|
+
return this._initialized;
|
|
13247
|
+
}
|
|
11962
13248
|
get initializePromise() {
|
|
11963
13249
|
return this._initializePromise;
|
|
11964
13250
|
}
|
|
@@ -11968,6 +13254,9 @@ class State extends HTMLElementBase {
|
|
|
11968
13254
|
get listPaths() {
|
|
11969
13255
|
return this._listPaths;
|
|
11970
13256
|
}
|
|
13257
|
+
get listKeys() {
|
|
13258
|
+
return this._listKeys;
|
|
13259
|
+
}
|
|
11971
13260
|
get elementPaths() {
|
|
11972
13261
|
return this._elementPaths;
|
|
11973
13262
|
}
|
|
@@ -11995,9 +13284,29 @@ class State extends HTMLElementBase {
|
|
|
11995
13284
|
}
|
|
11996
13285
|
return this._rootNode;
|
|
11997
13286
|
}
|
|
13287
|
+
/**
|
|
13288
|
+
* `rootNode` を保持しているか = `createState` を呼んでよいか(§1.9)。
|
|
13289
|
+
* disconnect で落ち、connect の冒頭で復活する。
|
|
13290
|
+
*/
|
|
13291
|
+
get hasRootNode() {
|
|
13292
|
+
return this._rootNode !== null;
|
|
13293
|
+
}
|
|
11998
13294
|
get boundComponentStateProp() {
|
|
11999
13295
|
return this._boundComponentStateProp;
|
|
12000
13296
|
}
|
|
13297
|
+
get boundComponent() {
|
|
13298
|
+
return this._boundComponent;
|
|
13299
|
+
}
|
|
13300
|
+
get hasMappedComponentState() {
|
|
13301
|
+
return this._hasMappedComponentState;
|
|
13302
|
+
}
|
|
13303
|
+
/**
|
|
13304
|
+
* この state の実体が innerState proxy であることを記録する。唯一の呼び手は
|
|
13305
|
+
* `bindWebComponent` の mapped 分岐(§1.8)。
|
|
13306
|
+
*/
|
|
13307
|
+
markComponentStateMapped() {
|
|
13308
|
+
this._hasMappedComponentState = true;
|
|
13309
|
+
}
|
|
12001
13310
|
get bindableEventMap() {
|
|
12002
13311
|
return this._bindableEventMap;
|
|
12003
13312
|
}
|
|
@@ -12054,8 +13363,15 @@ class State extends HTMLElementBase {
|
|
|
12054
13363
|
}
|
|
12055
13364
|
setPathInfo(path, bindingType) {
|
|
12056
13365
|
if (bindingType === "for") {
|
|
13366
|
+
const isNewListPath = !this._listPaths.has(path);
|
|
12057
13367
|
this._listPaths.add(path);
|
|
12058
13368
|
this._elementPaths.add(path + '.' + WILDCARD);
|
|
13369
|
+
// mapped な bind-component の子が回している for は、配列の実体を親スコープが
|
|
13370
|
+
// 持っている。親の依存 walk / swap 判定はどちらも「その state 要素の」
|
|
13371
|
+
// listPaths・elementPaths を見るので、マップ先のパスにも同じ宣言を届ける(§1.8)。
|
|
13372
|
+
if (isNewListPath && this._hasMappedComponentState) {
|
|
13373
|
+
propagateListPathToOuterState(this, path);
|
|
13374
|
+
}
|
|
12059
13375
|
}
|
|
12060
13376
|
if (!this._pathSet.has(path)) {
|
|
12061
13377
|
const pathInfo = getPathInfo(path);
|
|
@@ -12332,11 +13648,13 @@ function getWcsManifest() {
|
|
|
12332
13648
|
],
|
|
12333
13649
|
reservedStateApi: [
|
|
12334
13650
|
STATE_BINDABLES_NAME,
|
|
13651
|
+
STATE_COMMANDS_NAME,
|
|
12335
13652
|
STATE_COMMAND_TOKENS_NAME,
|
|
12336
13653
|
STATE_COMMAND_NAMESPACE_NAME,
|
|
12337
13654
|
STATE_EVENT_TOKENS_NAME,
|
|
12338
13655
|
STATE_ON_NAME,
|
|
12339
13656
|
STATE_STREAMS_NAME,
|
|
13657
|
+
STATE_LIST_KEYS_NAME,
|
|
12340
13658
|
STATE_STREAM_STATUS_NAMESPACE_NAME,
|
|
12341
13659
|
STATE_STREAM_ERROR_NAMESPACE_NAME,
|
|
12342
13660
|
],
|