@wcstack/state 1.21.3 → 1.21.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +8 -0
- package/dist/index.esm.js +553 -101
- package/dist/index.esm.js.map +1 -1
- package/dist/index.esm.min.js +1 -1
- package/dist/index.esm.min.js.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -83,6 +83,9 @@ function setConfig(partialConfig) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
const bindingPromiseByNode = new WeakMap();
|
|
86
|
+
// resolve 済みマーク。エントリ未生成のまま resolve されたノードは、後から
|
|
87
|
+
// wait された時に「生成して即 resolve」で追いつく。
|
|
88
|
+
const resolvedNodes = new WeakSet();
|
|
86
89
|
let id$1 = 0;
|
|
87
90
|
function getInitializeBindingPromiseByNode(node) {
|
|
88
91
|
let bindingPromise = bindingPromiseByNode.get(node) || null;
|
|
@@ -99,6 +102,9 @@ function getInitializeBindingPromiseByNode(node) {
|
|
|
99
102
|
resolve: resolveFn
|
|
100
103
|
};
|
|
101
104
|
bindingPromiseByNode.set(node, bindingPromise);
|
|
105
|
+
if (resolvedNodes.has(node)) {
|
|
106
|
+
bindingPromise.resolve();
|
|
107
|
+
}
|
|
102
108
|
return bindingPromise;
|
|
103
109
|
}
|
|
104
110
|
async function waitInitializeBinding(node) {
|
|
@@ -106,8 +112,15 @@ async function waitInitializeBinding(node) {
|
|
|
106
112
|
await bindingPromise.promise;
|
|
107
113
|
}
|
|
108
114
|
function resolveInitializedBinding(node) {
|
|
109
|
-
|
|
110
|
-
|
|
115
|
+
// ホットパス: リスト行では全 subscriber ノードがここを通るが、await する消費者
|
|
116
|
+
// (boundComponent / shadowRoot host)はほぼ居ない。既存エントリが無ければ
|
|
117
|
+
// Promise+closure を生成せず resolve 済みマークだけ残す(15 万個級の割り当て削減)。
|
|
118
|
+
const existing = bindingPromiseByNode.get(node);
|
|
119
|
+
if (typeof existing !== "undefined") {
|
|
120
|
+
existing.resolve();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
resolvedNodes.add(node);
|
|
111
124
|
}
|
|
112
125
|
|
|
113
126
|
const DELIMITER = '.';
|
|
@@ -575,7 +588,13 @@ function getBindingInfos(node, parseBindingTextResults) {
|
|
|
575
588
|
});
|
|
576
589
|
}
|
|
577
590
|
else {
|
|
578
|
-
|
|
591
|
+
// フラグメント登録時に事前正規化済みの Text ノードはそのまま replaceNode に
|
|
592
|
+
// 使う(node === replaceNode なら replaceToReplaceNode は no-op)。
|
|
593
|
+
// 実 DOM 上の wcs-text コメント(非フラグメント経路)は従来どおり
|
|
594
|
+
// 空 Text を生成して実行時に差し替える。
|
|
595
|
+
const replaceNode = node.nodeType === Node.TEXT_NODE
|
|
596
|
+
? node
|
|
597
|
+
: document.createTextNode('');
|
|
579
598
|
bindingInfos.push({
|
|
580
599
|
...parseBindingTextResult,
|
|
581
600
|
node: node,
|
|
@@ -3129,6 +3148,63 @@ function detachTwowayEventHandler(binding) {
|
|
|
3129
3148
|
}
|
|
3130
3149
|
}
|
|
3131
3150
|
|
|
3151
|
+
// framework 自身が detach し明示的に解体(deactivate/unmount)したノード。
|
|
3152
|
+
// BindingOwner の MutationObserver は削除サブツリー走査でこれらをスキップする。
|
|
3153
|
+
//
|
|
3154
|
+
// 根拠: 削除時の handleRemovedNode は binding を dispose するだけ(DOM 構造変更も
|
|
3155
|
+
// connect-snapshot 依存も無い)で、framework が unmount 経路で既に dispose 済みの
|
|
3156
|
+
// content に対しては純粋な冗長走査(forEachInclusive で削除サブツリー全体を歩く)に
|
|
3157
|
+
// なる。create(追加)経路は two-way の connect-time snapshot を observer に依存する
|
|
3158
|
+
// ため対象外だが、削除は依存が無いため安全に飛ばせる。
|
|
3159
|
+
//
|
|
3160
|
+
// マークは observer が削除を配送した時点で消費(削除)する。マーク〜配送の間隔は
|
|
3161
|
+
// 単一 microtask であり、その間に外部 DOM 変異は割り込めない(framework の drain は
|
|
3162
|
+
// 同期)ため、マークは framework 由来の削除にしか一致しない。
|
|
3163
|
+
const observerSkipNodes = new WeakSet();
|
|
3164
|
+
function markObserverSkipOnRemove(node) {
|
|
3165
|
+
observerSkipNodes.add(node);
|
|
3166
|
+
}
|
|
3167
|
+
// マーク済みなら true を返しつつマークを消費する。未マークなら false。
|
|
3168
|
+
function consumeObserverSkipOnRemove(node) {
|
|
3169
|
+
if (!observerSkipNodes.has(node)) {
|
|
3170
|
+
return false;
|
|
3171
|
+
}
|
|
3172
|
+
observerSkipNodes.delete(node);
|
|
3173
|
+
return true;
|
|
3174
|
+
}
|
|
3175
|
+
// framework 自身がマウント(Content.appendTo / mountAfter)したノード。
|
|
3176
|
+
// 追加サブツリー走査の実質の仕事は connect-snapshot 待ち(observationPending)の
|
|
3177
|
+
// record への配送だけで、record 自体は同期マウント(activateContent → start)で
|
|
3178
|
+
// observer flush より先に active 済み。よって待ちがグローバルに 1 つも無ければ
|
|
3179
|
+
// 追加側走査も冗長であり丸ごとスキップできる(削除側スキップの対称形)。
|
|
3180
|
+
// マーク〜配送が単一 microtask で外部変異が割り込めない前提も削除側と同じ。
|
|
3181
|
+
const observerSkipAddedNodes = new WeakSet();
|
|
3182
|
+
function markObserverSkipOnAdd(node) {
|
|
3183
|
+
observerSkipAddedNodes.add(node);
|
|
3184
|
+
}
|
|
3185
|
+
// マーク済みなら true を返しつつマークを消費する(削除側と同じ one-shot 契約)。
|
|
3186
|
+
function consumeObserverSkipOnAdd(node) {
|
|
3187
|
+
if (!observerSkipAddedNodes.has(node)) {
|
|
3188
|
+
return false;
|
|
3189
|
+
}
|
|
3190
|
+
observerSkipAddedNodes.delete(node);
|
|
3191
|
+
return true;
|
|
3192
|
+
}
|
|
3193
|
+
// connect-snapshot 待ち(two-way sync=connect で未接続のまま activate された record)の
|
|
3194
|
+
// グローバル件数。> 0 の間は追加側スキップを無効化して従来走査に戻す。
|
|
3195
|
+
// increment は settleInitialRecord、decrement は readProducerSnapshot(消化時)と
|
|
3196
|
+
// runTeardowns(未消化のまま終端した record のリーク防止)が担う。
|
|
3197
|
+
let pendingObservationCount = 0;
|
|
3198
|
+
function incrementPendingObservation() {
|
|
3199
|
+
pendingObservationCount++;
|
|
3200
|
+
}
|
|
3201
|
+
function decrementPendingObservation() {
|
|
3202
|
+
pendingObservationCount--;
|
|
3203
|
+
}
|
|
3204
|
+
function hasPendingObservation() {
|
|
3205
|
+
return pendingObservationCount > 0;
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3132
3208
|
/**
|
|
3133
3209
|
* Shares one CustomElementRegistry.whenDefined() continuation per registry/tag.
|
|
3134
3210
|
* Waiters can be removed independently, so a never-defined tag does not retain
|
|
@@ -3226,12 +3302,22 @@ function parseSyncOn(value) {
|
|
|
3226
3302
|
function hasInitialSyncModifier(binding) {
|
|
3227
3303
|
return binding.propModifiers.some((modifier) => modifier.includes("="));
|
|
3228
3304
|
}
|
|
3305
|
+
// 頻出ポリシー(修飾子なしの通常バインディング)の凍結シングルトン。リスト行では
|
|
3306
|
+
// binding ごとに resolveInitialSyncPolicy が走るため、毎回のオブジェクト割り当てを
|
|
3307
|
+
// 避ける(record.initialPolicy は読み取り専用でしか使われない)。
|
|
3308
|
+
const STATE_CALL_POLICY = Object.freeze({ authority: "state", syncOn: "call", observable: false });
|
|
3309
|
+
const NONE_CALL_POLICY = Object.freeze({ authority: "none", syncOn: "call", observable: false });
|
|
3310
|
+
function statePolicy(authority, syncOn) {
|
|
3311
|
+
if (authority === "state" && syncOn === "call")
|
|
3312
|
+
return STATE_CALL_POLICY;
|
|
3313
|
+
return { authority, syncOn, observable: false };
|
|
3314
|
+
}
|
|
3229
3315
|
function resolveInitialSyncPolicy(binding) {
|
|
3230
3316
|
if (!config.enableDirectionalInitialSync) {
|
|
3231
3317
|
if (hasInitialSyncModifier(binding)) {
|
|
3232
3318
|
raiseError("init=/sync= modifiers require enableDirectionalInitialSync.");
|
|
3233
3319
|
}
|
|
3234
|
-
return
|
|
3320
|
+
return STATE_CALL_POLICY;
|
|
3235
3321
|
}
|
|
3236
3322
|
const explicitAuthority = parseAuthority(readOption(binding, "init"));
|
|
3237
3323
|
const syncOn = parseSyncOn(readOption(binding, "sync"));
|
|
@@ -3239,7 +3325,7 @@ function resolveInitialSyncPolicy(binding) {
|
|
|
3239
3325
|
if (explicitAuthority !== null && explicitAuthority !== "none") {
|
|
3240
3326
|
raiseError("Event bindings only allow init=none.");
|
|
3241
3327
|
}
|
|
3242
|
-
return { authority: "none", syncOn, observable: false };
|
|
3328
|
+
return syncOn === "call" ? NONE_CALL_POLICY : { authority: "none", syncOn, observable: false };
|
|
3243
3329
|
}
|
|
3244
3330
|
// command.<name>: $command.<method> は命令的な command-token 配線。bindingType は
|
|
3245
3331
|
// "prop" だが propName ("command.<name>") は wcBindable property ではないため、下の
|
|
@@ -3247,17 +3333,17 @@ function resolveInitialSyncPolicy(binding) {
|
|
|
3247
3333
|
// 持たない配線なので、現行互換の "state" authority を返す(command token は従来通り
|
|
3248
3334
|
// 初期 apply で配線される)。
|
|
3249
3335
|
if (binding.propSegments[0] === "command") {
|
|
3250
|
-
return
|
|
3336
|
+
return statePolicy("state", syncOn);
|
|
3251
3337
|
}
|
|
3252
3338
|
if (binding.bindingType !== "prop") {
|
|
3253
3339
|
if (explicitAuthority !== null && explicitAuthority !== "state" && explicitAuthority !== "none") {
|
|
3254
3340
|
raiseError(`Binding type "${binding.bindingType}" does not support init=${explicitAuthority}.`);
|
|
3255
3341
|
}
|
|
3256
|
-
return
|
|
3342
|
+
return statePolicy(explicitAuthority ?? "state", syncOn);
|
|
3257
3343
|
}
|
|
3258
3344
|
const declaration = readBindableDeclaration(binding.node);
|
|
3259
3345
|
if (declaration === null) {
|
|
3260
|
-
return
|
|
3346
|
+
return statePolicy(explicitAuthority ?? "state", syncOn);
|
|
3261
3347
|
}
|
|
3262
3348
|
const hasOutput = declaration.knownProperties.has(binding.propName);
|
|
3263
3349
|
const hasInput = declaration.declaredInputs.has(binding.propName);
|
|
@@ -3332,6 +3418,11 @@ let nextRecordId = 0;
|
|
|
3332
3418
|
let nextGeneration = 0;
|
|
3333
3419
|
const recordByBinding = new WeakMap();
|
|
3334
3420
|
const sessionByRoot = new WeakMap();
|
|
3421
|
+
// binding の構造キーは不変フィールドのみから決まる。リスト行の初期化では同一 binding に
|
|
3422
|
+
// 対し remember() が2回呼ばれる(createContent 内 initializeBindingsByFragment と
|
|
3423
|
+
// activateContent の registerAddress 目的の initialize)ため、2度目の文字列生成を避けるべく
|
|
3424
|
+
// binding 単位でメモ化する。プロファイル上 bindingKey は create-10k の JS 自己時間で上位。
|
|
3425
|
+
const bindingKeyByBinding = new WeakMap();
|
|
3335
3426
|
// node → その node に関心を持つ session(anchor として binding を覚えている、
|
|
3336
3427
|
// または定義待ちタスクを抱えている)。BindingOwner は mutation で増減した
|
|
3337
3428
|
// サブツリーを1回だけ走査し、ここに登録された session だけへ per-node 配送する。
|
|
@@ -3366,6 +3457,11 @@ function forEachInterestedSession(node, callback) {
|
|
|
3366
3457
|
}
|
|
3367
3458
|
function forEachInclusive(root, callback) {
|
|
3368
3459
|
callback(root);
|
|
3460
|
+
// 葉ノード(fragment 一括挿入時のテキスト・空セル等が大多数)では
|
|
3461
|
+
// Array.from(childNodes) の空配列アロケーションを避ける。callback が子を
|
|
3462
|
+
// 追加しうるため firstChild は callback 後に判定する(従来と同一意味論)。
|
|
3463
|
+
if (root.firstChild === null)
|
|
3464
|
+
return;
|
|
3369
3465
|
for (const child of Array.from(root.childNodes)) {
|
|
3370
3466
|
forEachInclusive(child, callback);
|
|
3371
3467
|
}
|
|
@@ -3402,6 +3498,11 @@ class BindingOwner {
|
|
|
3402
3498
|
// 検査へ進める。contains は O(木の深さ) なので、関心の無い node で呼ばない。
|
|
3403
3499
|
const reconnected = [];
|
|
3404
3500
|
for (const subtree of removed) {
|
|
3501
|
+
// framework が unmount した削除サブツリーは binding を明示 dispose 済みなので
|
|
3502
|
+
// observer 側の冗長走査(forEachInclusive で全 node を歩き handleRemovedNode を
|
|
3503
|
+
// 呼ぶ)を丸ごとスキップする。clear/大量 delete のホットスポット短縮。
|
|
3504
|
+
if (consumeObserverSkipOnRemove(subtree))
|
|
3505
|
+
continue;
|
|
3405
3506
|
forEachInclusive(subtree, (node) => {
|
|
3406
3507
|
forEachInterestedSession(node, (session) => {
|
|
3407
3508
|
if (this.root.contains(node))
|
|
@@ -3411,6 +3512,11 @@ class BindingOwner {
|
|
|
3411
3512
|
});
|
|
3412
3513
|
}
|
|
3413
3514
|
for (const subtree of added) {
|
|
3515
|
+
// framework がマウントしたサブツリーは record が同期 activate 済みで、追加側
|
|
3516
|
+
// 走査の実質の仕事は connect-snapshot 待ちへの配送だけ。待ちがグローバルに
|
|
3517
|
+
// 無ければ丸ごとスキップする(待ちがあればマークだけ消費して従来走査に戻す)。
|
|
3518
|
+
if (consumeObserverSkipOnAdd(subtree) && !hasPendingObservation())
|
|
3519
|
+
continue;
|
|
3414
3520
|
forEachInclusive(subtree, (node) => {
|
|
3415
3521
|
forEachInterestedSession(node, (session) => {
|
|
3416
3522
|
if (!this.root.contains(node))
|
|
@@ -3483,6 +3589,41 @@ class BindingSession {
|
|
|
3483
3589
|
}
|
|
3484
3590
|
return initialized.filter((binding) => this.shouldApplyState(binding));
|
|
3485
3591
|
}
|
|
3592
|
+
/**
|
|
3593
|
+
* activateContent 専用の再活性化パス。createContent 側の initialize で
|
|
3594
|
+
* remember 済みの binding 配列(bindingsByContent がそのまま保持する同一オブジェクト)
|
|
3595
|
+
* にだけ使える前提で、remember の再実行(キー照合・options マージ・興味登録)を省き、
|
|
3596
|
+
* 必要な仕事だけ行う: 初回活性化はアドレス登録+初期同期、pool 再利用(disposed)は
|
|
3597
|
+
* start による再構築、未知の binding は防御的に従来 initialize へ倒す。
|
|
3598
|
+
*/
|
|
3599
|
+
activate(bindings) {
|
|
3600
|
+
for (const binding of bindings) {
|
|
3601
|
+
const record = recordByBinding.get(binding);
|
|
3602
|
+
if (typeof record !== "undefined" && record.session === this
|
|
3603
|
+
&& record.phase !== "disposed" && record.phase !== "failed") {
|
|
3604
|
+
if (record.address === null) {
|
|
3605
|
+
// 初回活性化(mountAfter 経路では anchor が接続済みのことがあるため、
|
|
3606
|
+
// 従来 initialize と同様に owner の存在をここで保証する)
|
|
3607
|
+
this.observe(record.anchor);
|
|
3608
|
+
record.options.registerAddress = true;
|
|
3609
|
+
this.registerAddress(record);
|
|
3610
|
+
}
|
|
3611
|
+
if (record.phase === "active")
|
|
3612
|
+
this.settleInitialRecord(record);
|
|
3613
|
+
this.settleConnectedSnapshot(record);
|
|
3614
|
+
continue;
|
|
3615
|
+
}
|
|
3616
|
+
const options = this.optionsByBinding.get(binding);
|
|
3617
|
+
if (typeof options === "undefined") {
|
|
3618
|
+
// この session で remember されていない binding(防御): 従来経路
|
|
3619
|
+
this.initialize([binding], { registerAddress: true, registerPathInfo: false, applyOnReconnect: false });
|
|
3620
|
+
continue;
|
|
3621
|
+
}
|
|
3622
|
+
// pool 再利用: record は disposed。活性化要件(アドレス登録)を昇格して再構築
|
|
3623
|
+
options.registerAddress = true;
|
|
3624
|
+
this.start(binding, options);
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3486
3627
|
shouldApplyState(binding) {
|
|
3487
3628
|
if (!config.enableDirectionalInitialSync) {
|
|
3488
3629
|
if (hasInitialSyncModifier(binding))
|
|
@@ -3570,6 +3711,35 @@ class BindingSession {
|
|
|
3570
3711
|
this.deferredByNode.get(task.node)?.delete(task);
|
|
3571
3712
|
}
|
|
3572
3713
|
}
|
|
3714
|
+
/**
|
|
3715
|
+
* wholesale destroy(全行クリアで teardown を GC に任せる高速経路)を適用して
|
|
3716
|
+
* よいか。定義待ち(DefinitionCoordinator の waiter / deferred spread タスク)は
|
|
3717
|
+
* 強参照 Map に閉包が残り、connect-snapshot 待ちは pending カウンタが戻らなく
|
|
3718
|
+
* なるため、1 つでもあれば従来経路(teardown 実行)に倒す。
|
|
3719
|
+
*/
|
|
3720
|
+
canWholesaleDestroy() {
|
|
3721
|
+
if (this.deferred.size > 0)
|
|
3722
|
+
return false;
|
|
3723
|
+
for (const record of this.records) {
|
|
3724
|
+
if (record.pendingDefinitions > 0 || record.observationPending)
|
|
3725
|
+
return false;
|
|
3726
|
+
}
|
|
3727
|
+
return true;
|
|
3728
|
+
}
|
|
3729
|
+
/**
|
|
3730
|
+
* 全 record を teardown を走らせずに終端化する(canWholesaleDestroy が true の
|
|
3731
|
+
* content 専用)。イベント listener・アドレス台帳・loopContext はノード/binding
|
|
3732
|
+
* もろとも GC で崩壊する(recordByBinding 以下は全て弱参照)。
|
|
3733
|
+
* handlerBindingRegistry のカウンタは減らないが、残るのはキー文字列と数値のみで
|
|
3734
|
+
* 実害はない設計(handlerBindingRegistry.ts の弱参照化コメント参照)。
|
|
3735
|
+
*/
|
|
3736
|
+
destroyRecords() {
|
|
3737
|
+
for (const record of this.records) {
|
|
3738
|
+
record.phase = "disposed";
|
|
3739
|
+
record.teardowns.clear();
|
|
3740
|
+
}
|
|
3741
|
+
this.records.clear();
|
|
3742
|
+
}
|
|
3573
3743
|
observe(node) {
|
|
3574
3744
|
const root = observableRootFor(node);
|
|
3575
3745
|
if (root === null)
|
|
@@ -3649,7 +3819,11 @@ class BindingSession {
|
|
|
3649
3819
|
known = new Map();
|
|
3650
3820
|
this.knownBindingsByNode.set(anchor, known);
|
|
3651
3821
|
}
|
|
3652
|
-
|
|
3822
|
+
let key = bindingKeyByBinding.get(binding);
|
|
3823
|
+
if (typeof key === "undefined") {
|
|
3824
|
+
key = bindingKey(binding);
|
|
3825
|
+
bindingKeyByBinding.set(binding, key);
|
|
3826
|
+
}
|
|
3653
3827
|
const remembered = known.get(key);
|
|
3654
3828
|
if (typeof remembered !== "undefined") {
|
|
3655
3829
|
const rememberedOptions = this.optionsByBinding.get(remembered);
|
|
@@ -3811,6 +3985,8 @@ class BindingSession {
|
|
|
3811
3985
|
&& record.info.node instanceof HTMLElement
|
|
3812
3986
|
&& !record.info.node.isConnected) {
|
|
3813
3987
|
record.observationPending = true;
|
|
3988
|
+
// 待ちが 1 件でもある間は追加側 observer スキップを無効化する
|
|
3989
|
+
incrementPendingObservation();
|
|
3814
3990
|
return;
|
|
3815
3991
|
}
|
|
3816
3992
|
this.readProducerSnapshot(record, policy.syncOn === "call");
|
|
@@ -3831,7 +4007,10 @@ class BindingSession {
|
|
|
3831
4007
|
return;
|
|
3832
4008
|
const sequence = record.eventSequence;
|
|
3833
4009
|
const value = target[name];
|
|
3834
|
-
record.observationPending
|
|
4010
|
+
if (record.observationPending) {
|
|
4011
|
+
record.observationPending = false;
|
|
4012
|
+
decrementPendingObservation();
|
|
4013
|
+
}
|
|
3835
4014
|
if (eventWins && record.eventSequence !== sequence)
|
|
3836
4015
|
return;
|
|
3837
4016
|
record.hasProducerValue = true;
|
|
@@ -3895,6 +4074,12 @@ class BindingSession {
|
|
|
3895
4074
|
this.records.delete(record);
|
|
3896
4075
|
}
|
|
3897
4076
|
runTeardowns(record) {
|
|
4077
|
+
// runTeardowns は record の終端(disposed / failed)でのみ呼ばれる。未消化の
|
|
4078
|
+
// connect-snapshot 待ちが残っていれば必ずカウンタを戻す(スキップ再有効化)。
|
|
4079
|
+
if (record.observationPending) {
|
|
4080
|
+
record.observationPending = false;
|
|
4081
|
+
decrementPendingObservation();
|
|
4082
|
+
}
|
|
3898
4083
|
const teardowns = Array.from(record.teardowns).reverse();
|
|
3899
4084
|
record.teardowns.clear();
|
|
3900
4085
|
for (const teardown of teardowns) {
|
|
@@ -4584,11 +4769,9 @@ function activateContent(content, loopContext, context) {
|
|
|
4584
4769
|
const bindings = getBindingsByContent(content);
|
|
4585
4770
|
const session = getBindingSessionByContent(content);
|
|
4586
4771
|
if (session !== null) {
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
applyOnReconnect: false,
|
|
4591
|
-
});
|
|
4772
|
+
// createContent 側の initialize で remember 済みの同一 binding 配列なので、
|
|
4773
|
+
// remember を再実行しない専用パスで活性化する(リスト行生成のホットパス)
|
|
4774
|
+
session.activate(bindings);
|
|
4592
4775
|
}
|
|
4593
4776
|
for (const binding of bindings) {
|
|
4594
4777
|
if (session === null) {
|
|
@@ -4675,6 +4858,10 @@ class Content {
|
|
|
4675
4858
|
}
|
|
4676
4859
|
appendTo(targetNode) {
|
|
4677
4860
|
for (const node of this._childNodeArray) {
|
|
4861
|
+
// framework 起点のマウントを observer に伝える。中間 fragment へ append する
|
|
4862
|
+
// 経路でも、後続の一括 insertBefore(fragment) の mutation record には
|
|
4863
|
+
// この top-level node が addedNodes として現れるため、ここでのマークが届く。
|
|
4864
|
+
markObserverSkipOnAdd(node);
|
|
4678
4865
|
targetNode.appendChild(node);
|
|
4679
4866
|
}
|
|
4680
4867
|
this._mounted = true;
|
|
@@ -4684,14 +4871,51 @@ class Content {
|
|
|
4684
4871
|
const nextSibling = targetNode.nextSibling;
|
|
4685
4872
|
if (parentNode) {
|
|
4686
4873
|
for (const node of this._childNodeArray) {
|
|
4874
|
+
markObserverSkipOnAdd(node);
|
|
4687
4875
|
parentNode.insertBefore(node, nextSibling);
|
|
4688
4876
|
}
|
|
4689
4877
|
}
|
|
4690
4878
|
this._mounted = true;
|
|
4691
4879
|
}
|
|
4880
|
+
tryDestroy() {
|
|
4881
|
+
const session = getBindingSessionByContent(this);
|
|
4882
|
+
// session 無し(SSR ハイドレーション産)や、定義待ち・connect-snapshot 待ちを
|
|
4883
|
+
// 抱える content は teardown 省略でリークするため従来経路に倒す。
|
|
4884
|
+
if (session === null || !session.canWholesaleDestroy()) {
|
|
4885
|
+
return false;
|
|
4886
|
+
}
|
|
4887
|
+
session.destroyRecords();
|
|
4888
|
+
for (const node of this._childNodeArray) {
|
|
4889
|
+
// unmount と同じ理由の observer 向け削除マーク(clear の一括削除でも
|
|
4890
|
+
// top-level node が mutation record の root に現れる)
|
|
4891
|
+
markObserverSkipOnRemove(node);
|
|
4892
|
+
if (node.parentNode !== null) {
|
|
4893
|
+
node.parentNode.removeChild(node);
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4896
|
+
const bindings = getBindingsByContent(this);
|
|
4897
|
+
for (const binding of bindings) {
|
|
4898
|
+
if (recursiveBindingTypes.has(binding.bindingType)) {
|
|
4899
|
+
const contents = getContentSetByNode(binding.node);
|
|
4900
|
+
for (const content of contents) {
|
|
4901
|
+
if (!content.tryDestroy()) {
|
|
4902
|
+
content.unmount();
|
|
4903
|
+
}
|
|
4904
|
+
}
|
|
4905
|
+
}
|
|
4906
|
+
}
|
|
4907
|
+
this._mounted = false;
|
|
4908
|
+
return true;
|
|
4909
|
+
}
|
|
4692
4910
|
unmount() {
|
|
4693
4911
|
getBindingSessionByContent(this)?.dispose();
|
|
4694
4912
|
for (const node of this._childNodeArray) {
|
|
4913
|
+
// framework 起点の削除であることを observer に伝える。clear の
|
|
4914
|
+
// parentNode.textContent='' 一括削除でも、この top-level node が
|
|
4915
|
+
// 削除サブツリーの root として mutation record に現れるため、ここで
|
|
4916
|
+
// マークしておけば observer の冗長走査をスキップできる。マークは
|
|
4917
|
+
// 同期実行中に立ち、observer は次 microtask で読むので順序は保証される。
|
|
4918
|
+
markObserverSkipOnRemove(node);
|
|
4695
4919
|
if (node.parentNode !== null) {
|
|
4696
4920
|
node.parentNode.removeChild(node);
|
|
4697
4921
|
}
|
|
@@ -4825,14 +5049,6 @@ function isPhysicallyAfter(lastNode, firstNode) {
|
|
|
4825
5049
|
return (position & Node.DOCUMENT_POSITION_FOLLOWING) !== 0
|
|
4826
5050
|
&& (position & Node.DOCUMENT_POSITION_DISCONNECTED) === 0;
|
|
4827
5051
|
}
|
|
4828
|
-
function getContent(node, listIndex) {
|
|
4829
|
-
let contentByListIndex = contentByListIndexByNode.get(node);
|
|
4830
|
-
if (typeof contentByListIndex === 'undefined') {
|
|
4831
|
-
return null;
|
|
4832
|
-
}
|
|
4833
|
-
const content = contentByListIndex.get(listIndex);
|
|
4834
|
-
return typeof content === 'undefined' ? null : content;
|
|
4835
|
-
}
|
|
4836
5052
|
function setContent(node, listIndex, content) {
|
|
4837
5053
|
let contentByListIndex = contentByListIndexByNode.get(node);
|
|
4838
5054
|
if (typeof contentByListIndex === 'undefined') {
|
|
@@ -4856,10 +5072,10 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
4856
5072
|
const lastValue = getLastListValueByAbsoluteStateAddress(absAddress);
|
|
4857
5073
|
const diff = createListDiff(listIndex, lastValue, newValue);
|
|
4858
5074
|
context.newListValueByAbsAddress.set(absAddress, Array.isArray(newValue) ? newValue : []);
|
|
4859
|
-
|
|
5075
|
+
const fullDelete = Array.isArray(lastValue)
|
|
4860
5076
|
&& lastValue.length === diff.deleteIndexSet.size
|
|
4861
|
-
&& diff.deleteIndexSet.size > 0
|
|
4862
|
-
|
|
5077
|
+
&& diff.deleteIndexSet.size > 0;
|
|
5078
|
+
if (fullDelete && bindingInfo.node.parentNode !== null) {
|
|
4863
5079
|
let isOnlyNode = isOnlyNodeInParentContentByNode.get(bindingInfo.node);
|
|
4864
5080
|
if (typeof isOnlyNode === 'undefined') {
|
|
4865
5081
|
const lastNode = lastNodeByNode.get(bindingInfo.node) || bindingInfo.node;
|
|
@@ -4872,13 +5088,39 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
4872
5088
|
parentNode.appendChild(bindingInfo.node);
|
|
4873
5089
|
}
|
|
4874
5090
|
}
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
5091
|
+
// 全削除時、プールに収まらない content は再利用されないため、per-binding の
|
|
5092
|
+
// teardown(listener 解除・アドレス台帳・loopContext 掃除)を丸ごと省略して
|
|
5093
|
+
// ノードごと GC に任せる(tryDestroy)。プール行きの分だけ従来どおり解体する
|
|
5094
|
+
// (プール行は binding が生存し続けるため address キャッシュのクリアが必須)。
|
|
5095
|
+
// content 台帳の WeakMap ビルトインは V8 プロファイルで本関数の self に計上される
|
|
5096
|
+
// ホットスポット: 外側の node→map 解決はループ外に持ち上げ、fullDelete(旧全行が
|
|
5097
|
+
// deleteIndexSet に載る=台帳の全エントリが消える)では per-index delete を廃して
|
|
5098
|
+
// 台帳ごと 1 回で手放す。
|
|
5099
|
+
let contentMap = contentByListIndexByNode.get(bindingInfo.node);
|
|
5100
|
+
let poolBudget = fullDelete
|
|
5101
|
+
? maxPooledContents - getPooledContents(bindingInfo).length
|
|
5102
|
+
: Number.POSITIVE_INFINITY;
|
|
5103
|
+
if (typeof contentMap !== 'undefined') {
|
|
5104
|
+
for (const deleteIndex of diff.deleteIndexSet) {
|
|
5105
|
+
const content = contentMap.get(deleteIndex);
|
|
5106
|
+
if (typeof content !== 'undefined') {
|
|
5107
|
+
if (poolBudget <= 0 && content.tryDestroy()) {
|
|
5108
|
+
deleteContentByNode(bindingInfo.node, content);
|
|
5109
|
+
}
|
|
5110
|
+
else {
|
|
5111
|
+
deactivateContent(content);
|
|
5112
|
+
content.unmount();
|
|
5113
|
+
setPooledContent(bindingInfo, content);
|
|
5114
|
+
poolBudget -= 1;
|
|
5115
|
+
}
|
|
5116
|
+
if (!fullDelete) {
|
|
5117
|
+
contentMap.delete(deleteIndex);
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
}
|
|
5121
|
+
if (fullDelete) {
|
|
5122
|
+
contentByListIndexByNode.delete(bindingInfo.node);
|
|
5123
|
+
contentMap = undefined;
|
|
4882
5124
|
}
|
|
4883
5125
|
}
|
|
4884
5126
|
let lastNode = bindingInfo.node;
|
|
@@ -4899,14 +5141,16 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
4899
5141
|
}
|
|
4900
5142
|
const ssrMode = inSsr();
|
|
4901
5143
|
const uuid = bindingInfo.uuid ?? '';
|
|
5144
|
+
// 追加行ごとの WeakMap 解決を避けるためプール配列も 1 回だけ引く(プールの配列
|
|
5145
|
+
// 実体は setPooledContent が一度作ったら不変なので、delete ループ後の参照で安定)
|
|
5146
|
+
const pooledContents = pooledContentsByNode.get(bindingInfo.node);
|
|
4902
5147
|
for (const index of diff.newIndexes) {
|
|
4903
5148
|
let content;
|
|
4904
5149
|
// add
|
|
4905
5150
|
if (diff.addIndexSet.has(index)) {
|
|
4906
5151
|
const stateAddress = createStateAddress(elementPathInfo, index);
|
|
4907
5152
|
loopContextStack.createLoopContext(stateAddress, (loopContext) => {
|
|
4908
|
-
|
|
4909
|
-
content = pooledContents.pop();
|
|
5153
|
+
content = typeof pooledContents !== 'undefined' ? pooledContents.pop() : undefined;
|
|
4910
5154
|
if (typeof content === 'undefined') {
|
|
4911
5155
|
content = createContent(bindingInfo);
|
|
4912
5156
|
}
|
|
@@ -4945,7 +5189,8 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
4945
5189
|
}
|
|
4946
5190
|
}
|
|
4947
5191
|
else {
|
|
4948
|
-
|
|
5192
|
+
// getContent 相当(undefined→null 正規化は後段の raiseError 判定が null 比較のため維持)
|
|
5193
|
+
content = (typeof contentMap !== 'undefined' ? contentMap.get(index) ?? null : null);
|
|
4949
5194
|
if (diff.changeIndexSet.has(index)) {
|
|
4950
5195
|
// change
|
|
4951
5196
|
const indexBindings = getIndexBindingsByContent(content);
|
|
@@ -4969,7 +5214,11 @@ function applyChangeToFor(bindingInfo, context, newValue) {
|
|
|
4969
5214
|
}
|
|
4970
5215
|
}
|
|
4971
5216
|
lastNode = content.lastNode || lastNode;
|
|
4972
|
-
|
|
5217
|
+
if (typeof contentMap === 'undefined') {
|
|
5218
|
+
contentMap = new WeakMap();
|
|
5219
|
+
contentByListIndexByNode.set(bindingInfo.node, contentMap);
|
|
5220
|
+
}
|
|
5221
|
+
contentMap.set(index, content);
|
|
4973
5222
|
}
|
|
4974
5223
|
lastNodeByNode.set(bindingInfo.node, lastNode);
|
|
4975
5224
|
if (fragment !== null) {
|
|
@@ -5458,6 +5707,10 @@ const applyChangeByBindingType = {
|
|
|
5458
5707
|
};
|
|
5459
5708
|
const fnByBinding = new WeakMap();
|
|
5460
5709
|
const deferredSelectBindingByBinding = new WeakMap();
|
|
5710
|
+
// 未 define カスタム要素チェックの確定メモ。customTag が無い、または define 済みを
|
|
5711
|
+
// 一度確認したら以後は不変(define は不可逆)なので apply 毎の getCustomElement /
|
|
5712
|
+
// registry 照会を省略できる。scoped registry を導入する場合はこの不可逆前提を再検討。
|
|
5713
|
+
const definedApplyVerifiedByBinding = new WeakMap();
|
|
5461
5714
|
function _applyChange(binding, context) {
|
|
5462
5715
|
const value = getValue(context.state, binding);
|
|
5463
5716
|
const filteredValue = getFilteredValue(value, binding.outFilters);
|
|
@@ -5543,16 +5796,20 @@ function applyChange(binding, context) {
|
|
|
5543
5796
|
if (binding.bindingType === "event") {
|
|
5544
5797
|
return;
|
|
5545
5798
|
}
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
if (
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5799
|
+
if (definedApplyVerifiedByBinding.get(binding) !== true) {
|
|
5800
|
+
const customTag = getCustomElement(binding.replaceNode);
|
|
5801
|
+
if (customTag) {
|
|
5802
|
+
if (getCustomElementRegistry()?.get(customTag) === undefined) {
|
|
5803
|
+
// 未 define のカスタム要素へは今は適用できない(accessor 未確立の要素に
|
|
5804
|
+
// 素の own property を書くと upgrade 後に class accessor を隠してしまう)。
|
|
5805
|
+
// whenDefined 後に最新 state 値で再適用する(two-way attach / deferred
|
|
5806
|
+
// spread と対称。docs/state-binding-init-races.md §2)。
|
|
5807
|
+
scheduleDeferredApply(binding, customTag);
|
|
5808
|
+
return;
|
|
5809
|
+
}
|
|
5555
5810
|
}
|
|
5811
|
+
// customTag 無し or define 済み確定 → 以後この検査を省略(不可逆)
|
|
5812
|
+
definedApplyVerifiedByBinding.set(binding, true);
|
|
5556
5813
|
}
|
|
5557
5814
|
// applyChangeFromBindings のグループ化ループが解決済みルートの一致を検証済みの
|
|
5558
5815
|
// 場合、stateName さえ一致すれば getRootNode の再解決(native 呼び出し)を省略
|
|
@@ -5905,8 +6162,24 @@ function getFragmentNodeInfos(fragment) {
|
|
|
5905
6162
|
const subscriberNodes = getSubscriberNodes(fragment);
|
|
5906
6163
|
for (const subscriberNode of subscriberNodes) {
|
|
5907
6164
|
const parseBindingTextResults = getParseBindTextResults(subscriberNode);
|
|
6165
|
+
let node = subscriberNode;
|
|
6166
|
+
// テンプレート登録時の事前正規化: text 専用の wcs-text コメントは、この時点で
|
|
6167
|
+
// 空 Text に置き換えておく。行 clone は最初から Text を持ち、getBindingInfos が
|
|
6168
|
+
// その Text を replaceNode に使うため、行ごとの createTextNode と start() 時の
|
|
6169
|
+
// replaceChild(コメント→Text 差し替え)が丸ごと不要になる。
|
|
6170
|
+
// 置換は同じ位置なので nodePath は不変。wcs-for/if 等の構造コメントは
|
|
6171
|
+
// アンカーとしてコメントのまま維持する(bindingType で判別)。
|
|
6172
|
+
// 非フラグメント経路(実 DOM 上のコメント)は従来どおり実行時に差し替える。
|
|
6173
|
+
if (subscriberNode.nodeType === Node.COMMENT_NODE
|
|
6174
|
+
&& parseBindingTextResults.length === 1
|
|
6175
|
+
&& parseBindingTextResults[0].bindingType === "text"
|
|
6176
|
+
&& subscriberNode.parentNode !== null) {
|
|
6177
|
+
const textNode = document.createTextNode("");
|
|
6178
|
+
subscriberNode.parentNode.replaceChild(textNode, subscriberNode);
|
|
6179
|
+
node = textNode;
|
|
6180
|
+
}
|
|
5908
6181
|
fragmnentNodeInfos.push({
|
|
5909
|
-
nodePath: getNodePath(
|
|
6182
|
+
nodePath: getNodePath(node),
|
|
5910
6183
|
parseBindTextResults: parseBindingTextResults,
|
|
5911
6184
|
});
|
|
5912
6185
|
}
|
|
@@ -6110,7 +6383,7 @@ async function buildBindings(root) {
|
|
|
6110
6383
|
}
|
|
6111
6384
|
}
|
|
6112
6385
|
|
|
6113
|
-
var version = "1.21.
|
|
6386
|
+
var version = "1.21.5";
|
|
6114
6387
|
var pkg = {
|
|
6115
6388
|
version: version};
|
|
6116
6389
|
|
|
@@ -8590,7 +8863,6 @@ const _cache = new Map();
|
|
|
8590
8863
|
class ResolvedAddress {
|
|
8591
8864
|
path;
|
|
8592
8865
|
segments;
|
|
8593
|
-
paths;
|
|
8594
8866
|
wildcardCount;
|
|
8595
8867
|
wildcardType;
|
|
8596
8868
|
wildcardIndexes;
|
|
@@ -8607,10 +8879,8 @@ class ResolvedAddress {
|
|
|
8607
8879
|
// Split path into individual segments
|
|
8608
8880
|
const segments = path.split(".");
|
|
8609
8881
|
const tmpPatternSegments = segments.slice();
|
|
8610
|
-
const paths = [];
|
|
8611
8882
|
let incompleteCount = 0; // Count of unresolved wildcards (*)
|
|
8612
8883
|
let completeCount = 0; // Count of resolved wildcards (numeric indexes)
|
|
8613
|
-
let lastPath = "";
|
|
8614
8884
|
let wildcardCount = 0;
|
|
8615
8885
|
let wildcardType = "none";
|
|
8616
8886
|
const wildcardIndexes = [];
|
|
@@ -8634,10 +8904,6 @@ class ResolvedAddress {
|
|
|
8634
8904
|
wildcardCount++;
|
|
8635
8905
|
}
|
|
8636
8906
|
}
|
|
8637
|
-
// Build cumulative path array
|
|
8638
|
-
lastPath += segment;
|
|
8639
|
-
paths.push(lastPath);
|
|
8640
|
-
lastPath += (i < segment.length - 1 ? "." : "");
|
|
8641
8907
|
}
|
|
8642
8908
|
// Generate pattern string with wildcards normalized
|
|
8643
8909
|
const structuredPath = tmpPatternSegments.join(".");
|
|
@@ -8659,7 +8925,6 @@ class ResolvedAddress {
|
|
|
8659
8925
|
}
|
|
8660
8926
|
this.path = path;
|
|
8661
8927
|
this.segments = segments;
|
|
8662
|
-
this.paths = paths;
|
|
8663
8928
|
this.wildcardCount = wildcardCount;
|
|
8664
8929
|
this.wildcardType = wildcardType;
|
|
8665
8930
|
this.wildcardIndexes = wildcardIndexes;
|
|
@@ -9002,6 +9267,7 @@ function _walkExpandWildcard(context, currentWildcardIndex, parentListIndex) {
|
|
|
9002
9267
|
}
|
|
9003
9268
|
}
|
|
9004
9269
|
}
|
|
9270
|
+
const EMPTY_INDEXES = [];
|
|
9005
9271
|
/**
|
|
9006
9272
|
* 静的子展開で訪問する listIndex 群を選ぶ。"diff" でも次の場合は全行に倒す:
|
|
9007
9273
|
* - diff に変化が一切見えない再代入(同一参照および内容同一コピーの再代入。
|
|
@@ -9012,26 +9278,60 @@ function _walkExpandWildcard(context, currentWildcardIndex, parentListIndex) {
|
|
|
9012
9278
|
*/
|
|
9013
9279
|
function selectExpansionIndexes(context, sourcePath, _lastValue, _newValue, listDiff) {
|
|
9014
9280
|
if (context.listExpansion === "full") {
|
|
9015
|
-
return listDiff.newIndexes;
|
|
9281
|
+
return { fullRows: listDiff.newIndexes, movedRows: null };
|
|
9016
9282
|
}
|
|
9017
9283
|
if (context.stateElement.crossRowListPaths?.has(sourcePath)) {
|
|
9018
|
-
return listDiff.newIndexes;
|
|
9284
|
+
return { fullRows: listDiff.newIndexes, movedRows: null };
|
|
9019
9285
|
}
|
|
9020
9286
|
if (listDiff.addIndexSet.size === 0 && listDiff.changeIndexSet.size === 0) {
|
|
9021
9287
|
// 追加も移動も無い。削除も無ければ「変化が見えない再代入」= リフレッシュ意図
|
|
9022
9288
|
if (listDiff.deleteIndexSet.size === 0) {
|
|
9023
|
-
return listDiff.newIndexes;
|
|
9289
|
+
return { fullRows: listDiff.newIndexes, movedRows: null };
|
|
9024
9290
|
}
|
|
9025
9291
|
// 削除のみ: 残存行は位置も値も不変なので展開しない
|
|
9026
|
-
return
|
|
9292
|
+
return { fullRows: EMPTY_INDEXES, movedRows: null };
|
|
9027
9293
|
}
|
|
9028
|
-
|
|
9029
|
-
|
|
9294
|
+
return { fullRows: listDiff.addIndexSet, movedRows: listDiff.changeIndexSet };
|
|
9295
|
+
}
|
|
9296
|
+
const EMPTY_PATH_INFOS = [];
|
|
9297
|
+
/**
|
|
9298
|
+
* 位置だけが変わった行(movedRows)で展開すべきパス群を求める。
|
|
9299
|
+
* `${listPath}.*` の静的 subtree を辿り、$1 等を読んだ実績のある getter
|
|
9300
|
+
* (indexDependentGetterPaths)だけを返す。行の同一性・listIndex は保たれ
|
|
9301
|
+
* index 以外の入力が不変なので、index を読まない getter / 値パスは再評価不要。
|
|
9302
|
+
* 戻り値:
|
|
9303
|
+
* - IPathInfo[](空可): この各パスだけを行の listIndex で展開する
|
|
9304
|
+
* - null: ネストしたワイルドカード配下に index 依存 getter がある
|
|
9305
|
+
* (listIndex の階数が合わず個別展開できない)→ 呼び出し側で行全体展開に倒す
|
|
9306
|
+
*/
|
|
9307
|
+
function getMovedRowExpansionPaths(context, wildcardPath, depPathInfo) {
|
|
9308
|
+
const indexGetters = context.stateElement.indexDependentGetterPaths;
|
|
9309
|
+
if (!indexGetters || indexGetters.size === 0) {
|
|
9310
|
+
return EMPTY_PATH_INFOS;
|
|
9030
9311
|
}
|
|
9031
|
-
|
|
9032
|
-
|
|
9312
|
+
let result = null;
|
|
9313
|
+
const queue = [wildcardPath];
|
|
9314
|
+
const seen = new Set(queue);
|
|
9315
|
+
for (let i = 0; i < queue.length; i++) {
|
|
9316
|
+
const path = queue[i];
|
|
9317
|
+
if (indexGetters.has(path)) {
|
|
9318
|
+
const pathInfo = getPathInfo(path);
|
|
9319
|
+
if (pathInfo.wildcardCount !== depPathInfo.wildcardCount) {
|
|
9320
|
+
return null;
|
|
9321
|
+
}
|
|
9322
|
+
(result ??= []).push(pathInfo);
|
|
9323
|
+
}
|
|
9324
|
+
const children = context.staticMap.get(path);
|
|
9325
|
+
if (children) {
|
|
9326
|
+
for (const child of children) {
|
|
9327
|
+
if (!seen.has(child)) {
|
|
9328
|
+
seen.add(child);
|
|
9329
|
+
queue.push(child);
|
|
9330
|
+
}
|
|
9331
|
+
}
|
|
9332
|
+
}
|
|
9033
9333
|
}
|
|
9034
|
-
return
|
|
9334
|
+
return result ?? EMPTY_PATH_INFOS;
|
|
9035
9335
|
}
|
|
9036
9336
|
function _walkDependency(context, startAddress, callback) {
|
|
9037
9337
|
const stack = [{ address: startAddress, depth: 0 }];
|
|
@@ -9065,11 +9365,35 @@ function _walkDependency(context, startAddress, callback) {
|
|
|
9065
9365
|
const absAddress = createAbsoluteStateAddress(absPathInfo, address.listIndex);
|
|
9066
9366
|
const lastValue = getLastListValueByAbsoluteStateAddress(absAddress);
|
|
9067
9367
|
const listDiff = createListDiff(address.listIndex, lastValue, newValue);
|
|
9068
|
-
|
|
9368
|
+
const selection = selectExpansionIndexes(context, sourcePath, lastValue, newValue, listDiff);
|
|
9369
|
+
for (const listIndex of selection.fullRows) {
|
|
9069
9370
|
const depAddress = createStateAddress(depPathInfo, listIndex);
|
|
9070
9371
|
context.result.add(depAddress);
|
|
9071
9372
|
nextEntries.push({ address: depAddress, depth: nextDepth });
|
|
9072
9373
|
}
|
|
9374
|
+
if (selection.movedRows !== null) {
|
|
9375
|
+
const movedPathInfos = getMovedRowExpansionPaths(context, dep, depPathInfo);
|
|
9376
|
+
if (movedPathInfos === null) {
|
|
9377
|
+
// ネスト配下に index 依存 getter: 安全側で行全体を展開(従来挙動)
|
|
9378
|
+
for (const listIndex of selection.movedRows) {
|
|
9379
|
+
const depAddress = createStateAddress(depPathInfo, listIndex);
|
|
9380
|
+
context.result.add(depAddress);
|
|
9381
|
+
nextEntries.push({ address: depAddress, depth: nextDepth });
|
|
9382
|
+
}
|
|
9383
|
+
}
|
|
9384
|
+
else if (movedPathInfos.length > 0) {
|
|
9385
|
+
// 位置のみ変わった行は index 依存 getter のパスだけを展開する
|
|
9386
|
+
for (const listIndex of selection.movedRows) {
|
|
9387
|
+
for (const pathInfo of movedPathInfos) {
|
|
9388
|
+
const depAddress = createStateAddress(pathInfo, listIndex);
|
|
9389
|
+
context.result.add(depAddress);
|
|
9390
|
+
nextEntries.push({ address: depAddress, depth: nextDepth });
|
|
9391
|
+
}
|
|
9392
|
+
}
|
|
9393
|
+
}
|
|
9394
|
+
// movedPathInfos が空: index を読む getter が subtree に無い =
|
|
9395
|
+
// 位置のみ変わった行の値は不変。展開・dirty 化とも不要。
|
|
9396
|
+
}
|
|
9073
9397
|
}
|
|
9074
9398
|
else {
|
|
9075
9399
|
const depAddress = createStateAddress(depPathInfo, address.listIndex);
|
|
@@ -9160,6 +9484,15 @@ function _walkDependency(context, startAddress, callback) {
|
|
|
9160
9484
|
}
|
|
9161
9485
|
}
|
|
9162
9486
|
function walkDependency(stateName, stateElement, startAddress, staticDependency, dynamicDependency, listPathSet, stateProxy, searchType, callback, options) {
|
|
9487
|
+
// 依存ゼロの葉パス(staticMap / dynamicMap にエントリ無し)は context や Set を
|
|
9488
|
+
// 割り当てず、開始アドレスの callback だけで完結する。リスト行の値書き込み
|
|
9489
|
+
// (update ホットパス)は set 毎にここを通る。開始アドレスへの callback は
|
|
9490
|
+
// 従来の walk 先頭と同一で、戻り値(依存アドレス群)も従来どおり空。
|
|
9491
|
+
const startPath = startAddress.pathInfo.path;
|
|
9492
|
+
if (!staticDependency.has(startPath) && !dynamicDependency.has(startPath)) {
|
|
9493
|
+
callback(startAddress);
|
|
9494
|
+
return [];
|
|
9495
|
+
}
|
|
9163
9496
|
const context = {
|
|
9164
9497
|
stateElement: stateElement,
|
|
9165
9498
|
staticMap: staticDependency,
|
|
@@ -9192,6 +9525,31 @@ function walkDependency(stateName, stateElement, startAddress, staticDependency,
|
|
|
9192
9525
|
* - finallyで必ず更新情報を登録し、再描画や依存解決に利用
|
|
9193
9526
|
* - getter/setter経由のスコープ切り替えも考慮した設計
|
|
9194
9527
|
*/
|
|
9528
|
+
// Phase 3: 書き込み時点の因果 context を update record に付与する。
|
|
9529
|
+
// binding 経由の書き込みは呼び出し元の dynamic scope から context を引き継ぎ、
|
|
9530
|
+
// binding 外からの API update は新しい transaction を開始する(設計書 §4 規則 1)。
|
|
9531
|
+
// 依存 walk で enqueue される派生アドレスも同じ書き込みの因果に属する。
|
|
9532
|
+
function notifyWrite(address, absAddress, receiver, handler) {
|
|
9533
|
+
const propagationContext = config.enablePropagationContext
|
|
9534
|
+
? (getCurrentPropagationContext() ?? beginPropagationTransaction(-1))
|
|
9535
|
+
: null;
|
|
9536
|
+
const updater = getUpdater();
|
|
9537
|
+
updater.enqueueAbsoluteAddress(absAddress, propagationContext);
|
|
9538
|
+
// 依存関係のあるキャッシュを無効化(ダーティ)、更新対象として登録
|
|
9539
|
+
walkDependency(handler.stateName, handler.stateElement, address, handler.stateElement.staticDependency, handler.stateElement.dynamicDependency, handler.stateElement.listPaths, receiver, "new", (depAddress) => {
|
|
9540
|
+
// キャッシュを無効化(ダーティ)
|
|
9541
|
+
if (depAddress === address)
|
|
9542
|
+
return;
|
|
9543
|
+
const absDepPathInfo = getAbsolutePathInfo(handler.stateElement, depAddress.pathInfo);
|
|
9544
|
+
const absDepAddress = createAbsoluteStateAddress(absDepPathInfo, depAddress.listIndex);
|
|
9545
|
+
dirtyCacheEntryByAbsoluteStateAddress(absDepAddress);
|
|
9546
|
+
// 更新対象として登録
|
|
9547
|
+
updater.enqueueAbsoluteAddress(absDepAddress, propagationContext);
|
|
9548
|
+
},
|
|
9549
|
+
// リスト置換時は追加行・位置変更行のみ展開する(未変更行の再訪を省く。
|
|
9550
|
+
// $postUpdate の手動リフレッシュは従来通り全行展開のまま)
|
|
9551
|
+
{ listExpansion: "diff" });
|
|
9552
|
+
}
|
|
9195
9553
|
function _setByAddress(target, address, absAddress, value, receiver, handler) {
|
|
9196
9554
|
try {
|
|
9197
9555
|
if (address.pathInfo.path in target) {
|
|
@@ -9226,29 +9584,7 @@ function _setByAddress(target, address, absAddress, value, receiver, handler) {
|
|
|
9226
9584
|
}
|
|
9227
9585
|
}
|
|
9228
9586
|
finally {
|
|
9229
|
-
|
|
9230
|
-
// binding 経由の書き込みは呼び出し元の dynamic scope から context を引き継ぎ、
|
|
9231
|
-
// binding 外からの API update は新しい transaction を開始する(設計書 §4 規則 1)。
|
|
9232
|
-
// 依存 walk で enqueue される派生アドレスも同じ書き込みの因果に属する。
|
|
9233
|
-
const propagationContext = config.enablePropagationContext
|
|
9234
|
-
? (getCurrentPropagationContext() ?? beginPropagationTransaction(-1))
|
|
9235
|
-
: null;
|
|
9236
|
-
const updater = getUpdater();
|
|
9237
|
-
updater.enqueueAbsoluteAddress(absAddress, propagationContext);
|
|
9238
|
-
// 依存関係のあるキャッシュを無効化(ダーティ)、更新対象として登録
|
|
9239
|
-
walkDependency(handler.stateName, handler.stateElement, address, handler.stateElement.staticDependency, handler.stateElement.dynamicDependency, handler.stateElement.listPaths, receiver, "new", (depAddress) => {
|
|
9240
|
-
// キャッシュを無効化(ダーティ)
|
|
9241
|
-
if (depAddress === address)
|
|
9242
|
-
return;
|
|
9243
|
-
const absDepPathInfo = getAbsolutePathInfo(handler.stateElement, depAddress.pathInfo);
|
|
9244
|
-
const absDepAddress = createAbsoluteStateAddress(absDepPathInfo, depAddress.listIndex);
|
|
9245
|
-
dirtyCacheEntryByAbsoluteStateAddress(absDepAddress);
|
|
9246
|
-
// 更新対象として登録
|
|
9247
|
-
updater.enqueueAbsoluteAddress(absDepAddress, propagationContext);
|
|
9248
|
-
},
|
|
9249
|
-
// リスト置換時は追加行・位置変更行のみ展開する(未変更行の再訪を省く。
|
|
9250
|
-
// $postUpdate の手動リフレッシュは従来通り全行展開のまま)
|
|
9251
|
-
{ listExpansion: "diff" });
|
|
9587
|
+
notifyWrite(address, absAddress, receiver, handler);
|
|
9252
9588
|
}
|
|
9253
9589
|
}
|
|
9254
9590
|
function _setByAddressWithSwap(target, address, absAddress, value, receiver, handler) {
|
|
@@ -9289,6 +9625,75 @@ function _setByAddressWithSwap(target, address, absAddress, value, receiver, han
|
|
|
9289
9625
|
}
|
|
9290
9626
|
function setByAddress(target, address, value, receiver, handler) {
|
|
9291
9627
|
const stateElement = handler.stateElement;
|
|
9628
|
+
const path = address.pathInfo.path;
|
|
9629
|
+
// --- fast path: 宣言済み getter/setter でも swap 対象でもない、親を持つ葉パス ---
|
|
9630
|
+
// 従来は same-value guard の値読み・hasByAddress・実書き込みがそれぞれ親チェーンを
|
|
9631
|
+
// 解決していた(キャッシュヒットでも getByAddress 呼び出しの固定費 ×3)。
|
|
9632
|
+
// 親を 1 回だけ解決し、同じ親オブジェクトに対して guard 判定と Reflect.set を行う。
|
|
9633
|
+
// 非オブジェクト親などの例外形は従来経路へ倒し、挙動差を作らない。
|
|
9634
|
+
if (!(path in target) && address.parentAddress !== null && !stateElement.elementPaths.has(path)) {
|
|
9635
|
+
const parentValue = getByAddress(target, address.parentAddress, receiver, handler);
|
|
9636
|
+
if (typeof parentValue === "object" && parentValue !== null) {
|
|
9637
|
+
// ワイルドカード末尾で listIndex が無い不正アドレスは、従来どおり
|
|
9638
|
+
// 書き込み時(enqueue 済みの try 内)に raiseError する → key は undefined のまま持ち回す
|
|
9639
|
+
const lastSegment = address.pathInfo.lastSegment;
|
|
9640
|
+
const key = lastSegment === WILDCARD
|
|
9641
|
+
? address.listIndex?.index
|
|
9642
|
+
: lastSegment;
|
|
9643
|
+
let devOldValue;
|
|
9644
|
+
let devHasOldValue = false;
|
|
9645
|
+
if (config.sameValueGuard && (value === null || typeof value !== "object")) {
|
|
9646
|
+
// hasByAddress と同じ「初期化済みスロットか」判定(undefined 格納と未初期化を区別)
|
|
9647
|
+
const has = key !== undefined && key in parentValue;
|
|
9648
|
+
const oldValue = key !== undefined ? parentValue[key] : undefined;
|
|
9649
|
+
if (has && Object.is(oldValue, value)) {
|
|
9650
|
+
return true;
|
|
9651
|
+
}
|
|
9652
|
+
devOldValue = oldValue;
|
|
9653
|
+
devHasOldValue = true;
|
|
9654
|
+
}
|
|
9655
|
+
const cacheable = address.pathInfo.wildcardCount > 0 ||
|
|
9656
|
+
stateElement.getterPaths.has(path);
|
|
9657
|
+
const absPathInfo = getAbsolutePathInfo(stateElement, address.pathInfo);
|
|
9658
|
+
const absAddress = createAbsoluteStateAddress(absPathInfo, address.listIndex);
|
|
9659
|
+
if (devtoolsSink !== null) {
|
|
9660
|
+
devtoolsSink({
|
|
9661
|
+
type: "state:write",
|
|
9662
|
+
absoluteAddress: absAddress,
|
|
9663
|
+
value,
|
|
9664
|
+
oldValue: devOldValue,
|
|
9665
|
+
hasOldValue: devHasOldValue,
|
|
9666
|
+
});
|
|
9667
|
+
}
|
|
9668
|
+
try {
|
|
9669
|
+
if (key === undefined) {
|
|
9670
|
+
raiseError(`address.listIndex?.index is undefined path: ${path}`);
|
|
9671
|
+
}
|
|
9672
|
+
return Reflect.set(parentValue, key, value);
|
|
9673
|
+
}
|
|
9674
|
+
finally {
|
|
9675
|
+
notifyWrite(address, absAddress, receiver, handler);
|
|
9676
|
+
if (cacheable) {
|
|
9677
|
+
setCacheEntryByAbsoluteStateAddress(absAddress, {
|
|
9678
|
+
value: value,
|
|
9679
|
+
dirty: false
|
|
9680
|
+
});
|
|
9681
|
+
}
|
|
9682
|
+
// DCC bindable イベントディスパッチ
|
|
9683
|
+
const eventName = stateElement.bindableEventMap[path];
|
|
9684
|
+
if (eventName) {
|
|
9685
|
+
const rootNode = stateElement.rootNode;
|
|
9686
|
+
if (rootNode instanceof ShadowRoot) {
|
|
9687
|
+
rootNode.host.dispatchEvent(new CustomEvent(eventName, {
|
|
9688
|
+
detail: value,
|
|
9689
|
+
bubbles: true,
|
|
9690
|
+
}));
|
|
9691
|
+
}
|
|
9692
|
+
}
|
|
9693
|
+
}
|
|
9694
|
+
}
|
|
9695
|
+
}
|
|
9696
|
+
// --- end fast path ---
|
|
9292
9697
|
// --- same-value guard (config.sameValueGuard・既定 ON) ---
|
|
9293
9698
|
// primitive 値かつ Object.is 同値なら、set / enqueue / walkDependency / DOM 適用 /
|
|
9294
9699
|
// $updatedCallback / DCC イベントを丸ごとスキップ(標準的なリアクティブ no-op)。
|
|
@@ -9710,13 +10115,33 @@ async function setLoopContextAsync(handler, loopContext, callback) {
|
|
|
9710
10115
|
// `$streamStatus.<name>` / `$streamError.<name>` の dotted パス判定用プレフィックス
|
|
9711
10116
|
const STREAM_STATUS_PATH_PREFIX = `${STATE_STREAM_STATUS_NAMESPACE_NAME}${DELIMITER}`;
|
|
9712
10117
|
const STREAM_ERROR_PATH_PREFIX = `${STATE_STREAM_ERROR_NAMESPACE_NAME}${DELIMITER}`;
|
|
10118
|
+
// symbol API のクロージャは handler(= proxy と 1:1、target/receiver 不変)ごとに
|
|
10119
|
+
// 使い回す。drain の getValue が binding ごとに getByAddressSymbol を引くため、
|
|
10120
|
+
// 毎回の新規クロージャ生成が GC 圧・固定費になっていた。
|
|
10121
|
+
const symbolApiCacheByHandler = new WeakMap();
|
|
10122
|
+
function getSymbolApiCache(handler) {
|
|
10123
|
+
let cache = symbolApiCacheByHandler.get(handler);
|
|
10124
|
+
if (typeof cache === "undefined") {
|
|
10125
|
+
cache = new Map();
|
|
10126
|
+
symbolApiCacheByHandler.set(handler, cache);
|
|
10127
|
+
}
|
|
10128
|
+
return cache;
|
|
10129
|
+
}
|
|
9713
10130
|
function get(target, prop, receiver, handler) {
|
|
9714
10131
|
const index = INDEX_BY_INDEX_NAME[prop];
|
|
9715
10132
|
if (typeof index !== "undefined") {
|
|
9716
10133
|
if (handler.addressStackLength === 0) {
|
|
9717
10134
|
raiseError(`No active state reference to get list index for "${prop.toString()}".`);
|
|
9718
10135
|
}
|
|
9719
|
-
const
|
|
10136
|
+
const lastAddress = handler.lastAddressStack;
|
|
10137
|
+
// getter 評価中のインデックス読み取りを記録する。位置だけが変わった行
|
|
10138
|
+
// (listDiff.changeIndexSet)は index 以外の入力が不変なので、walkDependency の
|
|
10139
|
+
// 静的子展開を「インデックスを読んだ getter の subtree」に限定できる。
|
|
10140
|
+
const lastInfo = lastAddress?.pathInfo;
|
|
10141
|
+
if (lastInfo && handler.stateElement?.getterPaths.has(lastInfo.path)) {
|
|
10142
|
+
handler.stateElement.addIndexDependentGetterPath?.(lastInfo.path);
|
|
10143
|
+
}
|
|
10144
|
+
const listIndex = lastAddress?.listIndex;
|
|
9720
10145
|
return listIndex?.indexes[index] ?? raiseError(`ListIndex not found: ${prop.toString()}`);
|
|
9721
10146
|
}
|
|
9722
10147
|
if (typeof prop === "string") {
|
|
@@ -9771,49 +10196,67 @@ function get(target, prop, receiver, handler) {
|
|
|
9771
10196
|
return getByAddress(target, stateAddress, receiver, handler);
|
|
9772
10197
|
}
|
|
9773
10198
|
else if (typeof prop === "symbol") {
|
|
10199
|
+
const cache = getSymbolApiCache(handler);
|
|
10200
|
+
const cached = cache.get(prop);
|
|
10201
|
+
if (typeof cached !== "undefined") {
|
|
10202
|
+
return cached;
|
|
10203
|
+
}
|
|
10204
|
+
let api;
|
|
9774
10205
|
switch (prop) {
|
|
9775
10206
|
case setLoopContextAsyncSymbol: {
|
|
9776
|
-
|
|
10207
|
+
api = (loopContext, callback = async () => { }) => {
|
|
9777
10208
|
return setLoopContextAsync(handler, loopContext, callback);
|
|
9778
10209
|
};
|
|
10210
|
+
break;
|
|
9779
10211
|
}
|
|
9780
10212
|
case setLoopContextSymbol: {
|
|
9781
|
-
|
|
10213
|
+
api = (loopContext, callback = () => { }) => {
|
|
9782
10214
|
return setLoopContext(handler, loopContext, callback);
|
|
9783
10215
|
};
|
|
10216
|
+
break;
|
|
9784
10217
|
}
|
|
9785
10218
|
case getByAddressSymbol: {
|
|
9786
|
-
|
|
10219
|
+
api = (address) => {
|
|
9787
10220
|
return getByAddress(target, address, receiver, handler);
|
|
9788
10221
|
};
|
|
10222
|
+
break;
|
|
9789
10223
|
}
|
|
9790
10224
|
case hasByAddressSymbol: {
|
|
9791
|
-
|
|
10225
|
+
api = (address) => {
|
|
9792
10226
|
return hasByAddress(target, address, receiver, handler);
|
|
9793
10227
|
};
|
|
10228
|
+
break;
|
|
9794
10229
|
}
|
|
9795
10230
|
case setByAddressSymbol: {
|
|
9796
|
-
|
|
10231
|
+
api = (address, value) => {
|
|
9797
10232
|
return setByAddress(target, address, value, receiver, handler);
|
|
9798
10233
|
};
|
|
10234
|
+
break;
|
|
9799
10235
|
}
|
|
9800
10236
|
case connectedCallbackSymbol: {
|
|
9801
|
-
|
|
9802
|
-
return connectedCallback(target,
|
|
10237
|
+
api = () => {
|
|
10238
|
+
return connectedCallback(target, connectedCallbackSymbol, receiver);
|
|
9803
10239
|
};
|
|
10240
|
+
break;
|
|
9804
10241
|
}
|
|
9805
10242
|
case disconnectedCallbackSymbol: {
|
|
9806
|
-
|
|
9807
|
-
return disconnectedCallback(target,
|
|
10243
|
+
api = () => {
|
|
10244
|
+
return disconnectedCallback(target, disconnectedCallbackSymbol, receiver);
|
|
9808
10245
|
};
|
|
10246
|
+
break;
|
|
9809
10247
|
}
|
|
9810
10248
|
case updatedCallbackSymbol: {
|
|
9811
|
-
|
|
10249
|
+
api = (refs) => {
|
|
9812
10250
|
return updatedCallback(target, refs, receiver, handler);
|
|
9813
10251
|
};
|
|
10252
|
+
break;
|
|
10253
|
+
}
|
|
10254
|
+
default: {
|
|
10255
|
+
return Reflect.get(target, prop, receiver);
|
|
9814
10256
|
}
|
|
9815
10257
|
}
|
|
9816
|
-
|
|
10258
|
+
cache.set(prop, api);
|
|
10259
|
+
return api;
|
|
9817
10260
|
}
|
|
9818
10261
|
}
|
|
9819
10262
|
|
|
@@ -10352,6 +10795,9 @@ class State extends HTMLElementBase {
|
|
|
10352
10795
|
// 他行を読む getter が検出されたリストパス(diff-filter 展開の全行フォールバック対象)。
|
|
10353
10796
|
// 依存マップ(static/dynamic)と同様に追加のみ・クリアしない(安全側に固定される)。
|
|
10354
10797
|
_crossRowListPaths = new Set();
|
|
10798
|
+
// $1 等のインデックスを読んだ getter パス(実行時検出)。位置のみ変わった行の
|
|
10799
|
+
// 静的子展開はこの集合の subtree に限定される。追加のみ・クリアしない(安全側)。
|
|
10800
|
+
_indexDependentGetterPaths = new Set();
|
|
10355
10801
|
_name = 'default';
|
|
10356
10802
|
_initialized = false;
|
|
10357
10803
|
_initializePromise;
|
|
@@ -10851,6 +11297,12 @@ class State extends HTMLElementBase {
|
|
|
10851
11297
|
addCrossRowListPath(path) {
|
|
10852
11298
|
this._crossRowListPaths.add(path);
|
|
10853
11299
|
}
|
|
11300
|
+
get indexDependentGetterPaths() {
|
|
11301
|
+
return this._indexDependentGetterPaths;
|
|
11302
|
+
}
|
|
11303
|
+
addIndexDependentGetterPath(path) {
|
|
11304
|
+
this._indexDependentGetterPaths.add(path);
|
|
11305
|
+
}
|
|
10854
11306
|
bindProperty(prop, desc) {
|
|
10855
11307
|
Object.defineProperty(this._state, prop, desc);
|
|
10856
11308
|
if (prop === STATE_UPDATED_CALLBACK_NAME) {
|