@wcstack/state 2.3.0 → 2.4.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 +89 -17
- package/README.md +89 -17
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.d.ts +144 -5
- package/dist/index.esm.js +2192 -167
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.esm.js +2 -0
- package/dist/wcs-manifest.json +1 -0
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -230,6 +230,7 @@ const STATE_EVENT_TOKENS_NAME = "$eventTokens";
|
|
|
230
230
|
const STATE_ON_NAME = "$on";
|
|
231
231
|
const STATE_STREAMS_NAME = "$streams";
|
|
232
232
|
const STATE_WATCH_NAME = "$watch";
|
|
233
|
+
const STATE_SCAN_NAME = "$scan";
|
|
233
234
|
const STATE_RECURSION_NAME = "$recursion";
|
|
234
235
|
/**
|
|
235
236
|
* 再帰ワイルドカード。オーサリング層($recursion 宣言・getter キー・API 引数)にだけ
|
|
@@ -2404,7 +2405,7 @@ function processDeferredNode(entry) {
|
|
|
2404
2405
|
|
|
2405
2406
|
let nextMountId = 0;
|
|
2406
2407
|
const MOUNT_DOLLAR_DECLARATIONS = [
|
|
2407
|
-
"$watch", "$streams", "$listKeys", "$updatedCallback", "$commandTokens", "$eventTokens", "$on",
|
|
2408
|
+
"$watch", "$streams", "$scan", "$listKeys", "$updatedCallback", "$commandTokens", "$eventTokens", "$on",
|
|
2408
2409
|
"$recursion",
|
|
2409
2410
|
];
|
|
2410
2411
|
const dollarDeclarationWarned = new Set();
|
|
@@ -3674,7 +3675,7 @@ function detachCheckboxEventHandler(binding) {
|
|
|
3674
3675
|
* 同期 throw はここを通らない(従来どおり呼び出し元へ伝播する)。プログラマエラーを
|
|
3675
3676
|
* loud に落とす `raiseError` の挙動は一切変えない。
|
|
3676
3677
|
*/
|
|
3677
|
-
function isThenable(value) {
|
|
3678
|
+
function isThenable$1(value) {
|
|
3678
3679
|
return ((typeof value === "object" || typeof value === "function") &&
|
|
3679
3680
|
value !== null &&
|
|
3680
3681
|
typeof value.then === "function");
|
|
@@ -3688,7 +3689,7 @@ function captureHandlerRejection(result, describe) {
|
|
|
3688
3689
|
// emit は subscriber ごとの戻り値配列。単一ハンドラの戻り値はそのまま届く。
|
|
3689
3690
|
const values = Array.isArray(result) ? result : [result];
|
|
3690
3691
|
for (const value of values) {
|
|
3691
|
-
if (!isThenable(value)) {
|
|
3692
|
+
if (!isThenable$1(value)) {
|
|
3692
3693
|
continue;
|
|
3693
3694
|
}
|
|
3694
3695
|
// Promise.resolve は native Promise をそのまま返すため、catch の登録によって
|
|
@@ -3764,12 +3765,12 @@ class EventToken extends Token {
|
|
|
3764
3765
|
}
|
|
3765
3766
|
}
|
|
3766
3767
|
|
|
3767
|
-
const registryByStateElement$
|
|
3768
|
+
const registryByStateElement$4 = new WeakMap();
|
|
3768
3769
|
function getOrCreateEventToken(stateElement, name) {
|
|
3769
|
-
let registry = registryByStateElement$
|
|
3770
|
+
let registry = registryByStateElement$4.get(stateElement);
|
|
3770
3771
|
if (typeof registry === "undefined") {
|
|
3771
3772
|
registry = new Map();
|
|
3772
|
-
registryByStateElement$
|
|
3773
|
+
registryByStateElement$4.set(stateElement, registry);
|
|
3773
3774
|
}
|
|
3774
3775
|
let token = registry.get(name);
|
|
3775
3776
|
if (typeof token === "undefined") {
|
|
@@ -3780,7 +3781,7 @@ function getOrCreateEventToken(stateElement, name) {
|
|
|
3780
3781
|
return token;
|
|
3781
3782
|
}
|
|
3782
3783
|
function clearEventTokenRegistry(stateElement) {
|
|
3783
|
-
registryByStateElement$
|
|
3784
|
+
registryByStateElement$4.delete(stateElement);
|
|
3784
3785
|
}
|
|
3785
3786
|
|
|
3786
3787
|
/**
|
|
@@ -6006,7 +6007,9 @@ function applyChangeToClass(binding, _context, newValue) {
|
|
|
6006
6007
|
*
|
|
6007
6008
|
* 既知の制約:
|
|
6008
6009
|
* - emit が来なければ stale subscriber は token に残り続ける(要素が GC されても subscriber 関数自体は残る)。
|
|
6009
|
-
* state
|
|
6010
|
+
* registry は state 要素をキーにした WeakMap なので、state 要素ごと GC されたときに解放される。
|
|
6011
|
+
* 切断では registry を捨てない — 捨てると、ルート <wcs-state> を付け直した後の emit が
|
|
6012
|
+
* 購読者の居ない新しい token に届き、命令が無言で止まる(#273)。
|
|
6010
6013
|
* element ライフサイクルに直接フックする手段が現状の binding 機構に無いため、能動的な purge は将来課題。
|
|
6011
6014
|
*/
|
|
6012
6015
|
const subscribedBindings = new WeakMap();
|
|
@@ -6103,6 +6106,14 @@ function getUUID() {
|
|
|
6103
6106
|
}
|
|
6104
6107
|
|
|
6105
6108
|
let version$1 = 0;
|
|
6109
|
+
/**
|
|
6110
|
+
* 親の付け替え(#256 の修理)の世代。`listIndexes` の WeakRef 連鎖は一度組んだら
|
|
6111
|
+
* 作り直されないので、祖先が付け替わったら子孫のキャッシュも無効にする必要がある。
|
|
6112
|
+
* `dirty`(version 比較)は `indexes` の再構築が消費してしまうため、連鎖専用の世代を持つ。
|
|
6113
|
+
*/
|
|
6114
|
+
let chainGeneration = 0;
|
|
6115
|
+
/** 値が一度も記録されていない行の印(`undefined` を持つ行と区別するため)。 */
|
|
6116
|
+
const NO_VALUE = Symbol("wcs.listIndex.noValue");
|
|
6106
6117
|
class ListIndex {
|
|
6107
6118
|
uuid = getUUID();
|
|
6108
6119
|
parentListIndex;
|
|
@@ -6112,18 +6123,36 @@ class ListIndex {
|
|
|
6112
6123
|
_version;
|
|
6113
6124
|
_indexes;
|
|
6114
6125
|
_listIndexes;
|
|
6126
|
+
_chainGeneration;
|
|
6127
|
+
/**
|
|
6128
|
+
* この行集合を最初に展開した親(`home`・#256)。付け替えても変わらない。退役した親から
|
|
6129
|
+
* 付け替えたあと、その親が戻ってきたら戻す先になる。既存の行集合を引き継ぐ行は、鋳造時の
|
|
6130
|
+
* 親ではなくその行集合の home を継ぐ(`createListDiff`)── 1 組の行集合に home は 1 つ。
|
|
6131
|
+
*/
|
|
6132
|
+
_homeParentListIndex;
|
|
6133
|
+
/**
|
|
6134
|
+
* この行が表しているリスト要素(#256)。差分が返すたびに付け直す。
|
|
6135
|
+
* 「同じ行が戻ってきた」を配列インスタンスをまたいで言えるのはこの値だけ ──
|
|
6136
|
+
* 行を戻す普通のやり方(新しい配列に同じ要素を並べ直す)は ListIndex を作り直すので、
|
|
6137
|
+
* 行オブジェクトの identity では判定できない。
|
|
6138
|
+
*/
|
|
6139
|
+
_value;
|
|
6115
6140
|
/**
|
|
6116
6141
|
* Creates a new ListIndex instance.
|
|
6117
6142
|
*
|
|
6118
6143
|
* @param parentListIndex - Parent list index for nested loops, or null for top-level
|
|
6119
6144
|
* @param index - Current index value in the loop
|
|
6145
|
+
* @param homeParentListIndex - Parent this row's set belongs to (#256)
|
|
6120
6146
|
*/
|
|
6121
|
-
constructor(parentListIndex, index) {
|
|
6147
|
+
constructor(parentListIndex, index, homeParentListIndex) {
|
|
6122
6148
|
this.parentListIndex = parentListIndex;
|
|
6123
6149
|
this.position = parentListIndex ? parentListIndex.position + 1 : 0;
|
|
6124
6150
|
this.length = this.position + 1;
|
|
6151
|
+
this._homeParentListIndex = homeParentListIndex;
|
|
6152
|
+
this._value = NO_VALUE;
|
|
6125
6153
|
this._index = index;
|
|
6126
6154
|
this._version = version$1;
|
|
6155
|
+
this._chainGeneration = chainGeneration;
|
|
6127
6156
|
}
|
|
6128
6157
|
/**
|
|
6129
6158
|
* Gets current index value.
|
|
@@ -6151,6 +6180,17 @@ class ListIndex {
|
|
|
6151
6180
|
get version() {
|
|
6152
6181
|
return this._version;
|
|
6153
6182
|
}
|
|
6183
|
+
/** 行集合を最初に展開した親(付け替えても変わらない)。 */
|
|
6184
|
+
get homeParentListIndex() {
|
|
6185
|
+
return this._homeParentListIndex;
|
|
6186
|
+
}
|
|
6187
|
+
/** この行が表しているリスト要素(未記録なら `NO_VALUE`)。 */
|
|
6188
|
+
get value() {
|
|
6189
|
+
return this._value;
|
|
6190
|
+
}
|
|
6191
|
+
set value(value) {
|
|
6192
|
+
this._value = value;
|
|
6193
|
+
}
|
|
6154
6194
|
/**
|
|
6155
6195
|
* Checks if parent indexes have changed since last access.
|
|
6156
6196
|
*
|
|
@@ -6196,8 +6236,9 @@ class ListIndex {
|
|
|
6196
6236
|
}
|
|
6197
6237
|
}
|
|
6198
6238
|
else {
|
|
6199
|
-
if (typeof this._listIndexes === "undefined") {
|
|
6239
|
+
if (typeof this._listIndexes === "undefined" || this._chainGeneration !== chainGeneration) {
|
|
6200
6240
|
this._listIndexes = [...this.parentListIndex.listIndexes, new WeakRef(this)];
|
|
6241
|
+
this._chainGeneration = chainGeneration;
|
|
6201
6242
|
}
|
|
6202
6243
|
}
|
|
6203
6244
|
return this._listIndexes;
|
|
@@ -6217,6 +6258,20 @@ class ListIndex {
|
|
|
6217
6258
|
* @param pos - Position index (0-based, negative for from end)
|
|
6218
6259
|
* @returns ListIndex at position or null if not found/garbage collected
|
|
6219
6260
|
*/
|
|
6261
|
+
/**
|
|
6262
|
+
* 退役した親を、同じ深さの生きた親へ差し替える(#256)。**行の identity は保つ**ので、
|
|
6263
|
+
* 描画済み content も、その行に紐づくバインドしていない DOM の状態も残る。
|
|
6264
|
+
* 添字の値は親が変われば変わりうる(並べ替えを伴う置換)ため、`indexes` と WeakRef 連鎖を
|
|
6265
|
+
* 捨て、version を進めて子孫の `dirty` を立てる。`_homeParentListIndex` は動かさない
|
|
6266
|
+
* ── 外した行が戻ってきたときに持ち主を返す先だから。
|
|
6267
|
+
*/
|
|
6268
|
+
reparent(parentListIndex) {
|
|
6269
|
+
this.parentListIndex = parentListIndex;
|
|
6270
|
+
this._indexes = undefined;
|
|
6271
|
+
this._listIndexes = undefined;
|
|
6272
|
+
this._version = ++version$1;
|
|
6273
|
+
chainGeneration++;
|
|
6274
|
+
}
|
|
6220
6275
|
at(pos) {
|
|
6221
6276
|
if (pos >= 0) {
|
|
6222
6277
|
return this.listIndexes[pos]?.deref() || null;
|
|
@@ -6231,15 +6286,203 @@ class ListIndex {
|
|
|
6231
6286
|
*
|
|
6232
6287
|
* @param parentListIndex - Parent list index for nested loops, or null for top-level
|
|
6233
6288
|
* @param index - Current index value in the loop
|
|
6289
|
+
* @param homeParentListIndex - Row set this row joins (#256); defaults to the minting parent
|
|
6234
6290
|
* @returns New IListIndex instance
|
|
6235
6291
|
*/
|
|
6236
|
-
function createListIndex(parentListIndex, index) {
|
|
6237
|
-
return new ListIndex(parentListIndex, index);
|
|
6292
|
+
function createListIndex(parentListIndex, index, homeParentListIndex = parentListIndex) {
|
|
6293
|
+
return new ListIndex(parentListIndex, index, homeParentListIndex);
|
|
6294
|
+
}
|
|
6295
|
+
/**
|
|
6296
|
+
* 台帳(`listIndexesByList`)専用の入口。行は必ずこのモジュールが鋳造した実体なので、
|
|
6297
|
+
* 到達不能な防御分岐を置かずに直接呼ぶ。
|
|
6298
|
+
*/
|
|
6299
|
+
function reparentListIndex(listIndex, parentListIndex) {
|
|
6300
|
+
listIndex.reparent(parentListIndex);
|
|
6301
|
+
}
|
|
6302
|
+
/**
|
|
6303
|
+
* 台帳専用の入口その 2。行集合の home を読む(`IListIndex` を広げないための cast ──
|
|
6304
|
+
* この型は dist/index.d.ts に出るので、内部だけの都合で公開面を増やさない)。
|
|
6305
|
+
*/
|
|
6306
|
+
function getHomeParentListIndex(listIndex) {
|
|
6307
|
+
return listIndex.homeParentListIndex;
|
|
6308
|
+
}
|
|
6309
|
+
/** 台帳専用の入口その 3。差分が返した行に、その行が表している要素を憶えさせる(#256)。 */
|
|
6310
|
+
function setListIndexValue(listIndex, value) {
|
|
6311
|
+
listIndex.value = value;
|
|
6312
|
+
}
|
|
6313
|
+
/**
|
|
6314
|
+
* 2 つの行が**同じリスト要素**を表しているか(#256)。
|
|
6315
|
+
* 差分を通っていない行(`hydrateBindings` / `setByAddress` が鋳造する行)は値を持たないので、
|
|
6316
|
+
* 未記録どうしは「同じ」と見なさない ── 値の無い行を取り違えて付け替えないため。
|
|
6317
|
+
* 行を書き換えない読むだけの判定なので、台帳の外からも使う: `$scan` の drain が、行のアドレスが
|
|
6318
|
+
* いまその位置の要素を表しているかを見る(scan/scanRuntime.ts の placementOf)。
|
|
6319
|
+
*/
|
|
6320
|
+
function isSameListIndexValue(listIndex, other) {
|
|
6321
|
+
const value = listIndex.value;
|
|
6322
|
+
return value !== NO_VALUE && value === other.value;
|
|
6238
6323
|
}
|
|
6239
6324
|
|
|
6325
|
+
/**
|
|
6326
|
+
* list/listIndexesByList.ts
|
|
6327
|
+
*
|
|
6328
|
+
* 行(`IListIndex`)の正本台帳。1 本の配列につき行集合は **1 組**。
|
|
6329
|
+
*
|
|
6330
|
+
* #256 が扱うのは「共有」ではなく「陳腐化」である。台帳はこの 2 つを分ける:
|
|
6331
|
+
*
|
|
6332
|
+
* - **生きた共有** ── 1 本の配列が 2 つの生きた親から到達できる形。行集合は 1 組のまま
|
|
6333
|
+
* 全員で共有する(どの親から読んでも同じ値が見える)。親ごとに私有の行集合を持たせると
|
|
6334
|
+
* 同じスロットに 2 本の絶対アドレスができ、片方へ書いた値がもう片方から**永久に**
|
|
6335
|
+
* 見えなくなる。共有そのものの帰結(親を読む getter が持ち主の行の文脈で評価される・
|
|
6336
|
+
* 1 スロットへ到達経路の数だけ書かれる)は残るが、それはデータが実際に共有されている
|
|
6337
|
+
* ことの帰結であって、**もう存在しない古い値**ではない。
|
|
6338
|
+
* - **陳腐化** ── 行オブジェクトだけを作り直す置換(`nodes.map(n => ({...n}))` は
|
|
6339
|
+
* `children` を参照ごと引き継ぐ)で、台帳の行がぶら下がる親が**退役した**形。読み手は
|
|
6340
|
+
* 新しい行の絶対アドレスを見るのに、書き手は行の親ポインタを遡って旧行の絶対アドレスを
|
|
6341
|
+
* dirty にするので、葉への書き込みが集計へ届かない(#256)。
|
|
6342
|
+
*
|
|
6343
|
+
* 判別は「台帳の行が持つ親が、生きた親集合に属するか」。属さないときだけ、**行の identity を
|
|
6344
|
+
* 保ったまま**新しい親へ付け替える(`reparentListIndex`)。作り直さないのは、消費者が
|
|
6345
|
+
* 描画済み content を行 ListIndex の identity で持っているため ── 作り直すと、著者が何も
|
|
6346
|
+
* 間違えていないページで行の DOM が丸ごと失われる(`<details>` の開閉のような、バインド
|
|
6347
|
+
* していない状態ごと)。
|
|
6348
|
+
*
|
|
6349
|
+
* 退役の signal は差分の `deleteIndexSet`(=エンジン自身が「この行はもう無い」と決めた
|
|
6350
|
+
* 集合)で、**復活の signal は同じ差分の `newIndexes`**。印は差分ごとに付け直され、
|
|
6351
|
+
* 消えない印は残らない(`retireListIndexes` / `reviveListIndexes` を `createListDiff` が
|
|
6352
|
+
* 対で呼ぶ)。
|
|
6353
|
+
*
|
|
6354
|
+
* 付け替えは**一方通行ではない**。行は自分の行集合を最初に展開した親(`home`)を憶えていて、
|
|
6355
|
+
* その home が**リストに戻ってきたら**持ち主を返す。これが無いと「行を削除して戻す」ページで
|
|
6356
|
+
* 持ち主が隣の行に移ったまま固定され、削除の履歴によって集計が凍る行が変わってしまう。
|
|
6357
|
+
*
|
|
6358
|
+
* 戻ってきたかどうかは **行オブジェクト(ListIndex)の identity ではなく、行が表している
|
|
6359
|
+
* リスト要素の identity** で判定する。同じ配列インスタンスを戻す綴りなら home そのものが
|
|
6360
|
+
* 生き返るが、行を戻す普通のやり方(新しい配列に同じ要素を並べ直す)では差分が ListIndex を
|
|
6361
|
+
* 作り直すので、ListIndex の identity で見ると home は永久に退役したままになる。
|
|
6362
|
+
* 要素そのものを作り直した行は**別の行**なので home には一致しない。生き残った行が
|
|
6363
|
+
* 1 つでもあればそちらへ戻るが、**全ての行を作り直す**綴り(`map(n => ({...n}))`)では
|
|
6364
|
+
* どの要素も一致せず、退役した親からの付け替えが位置に対して働く。
|
|
6365
|
+
*/
|
|
6240
6366
|
const listIndexesByList = new WeakMap();
|
|
6367
|
+
/**
|
|
6368
|
+
* 差分で `newIndexes` から外れた行 = 消費者が画面から外した行。
|
|
6369
|
+
* 「生きた親集合に属さない」の判定材料。復活した行はこの集合から外れる。
|
|
6370
|
+
*/
|
|
6371
|
+
const retiredListIndexes = new WeakSet();
|
|
6372
|
+
/** 差分が捨てた行を退役として記録する(`createListDiff` が呼ぶ)。 */
|
|
6373
|
+
function retireListIndexes(listIndexes) {
|
|
6374
|
+
for (const listIndex of listIndexes) {
|
|
6375
|
+
retiredListIndexes.add(listIndex);
|
|
6376
|
+
}
|
|
6377
|
+
}
|
|
6378
|
+
/**
|
|
6379
|
+
* 差分が「いま生きている」と返した行の退役印を落とす(`createListDiff` が呼ぶ)。
|
|
6380
|
+
* 同じ配列インスタンスを戻す・タブを切り替えて戻る等で、退役した行は実際に生き返る。
|
|
6381
|
+
*/
|
|
6382
|
+
function reviveListIndexes(listIndexes) {
|
|
6383
|
+
for (const listIndex of listIndexes) {
|
|
6384
|
+
retiredListIndexes.delete(listIndex);
|
|
6385
|
+
}
|
|
6386
|
+
}
|
|
6387
|
+
/**
|
|
6388
|
+
* 差分がリストから外し、まだ戻っていない行か。読むだけの判定で、`$scan` の drain が
|
|
6389
|
+
* 行のアドレスの指す行がもうリストに居ないかを見る(scan/scanRuntime.ts の placementOf)。
|
|
6390
|
+
*/
|
|
6391
|
+
function isRetiredListIndex(listIndex) {
|
|
6392
|
+
return retiredListIndexes.has(listIndex);
|
|
6393
|
+
}
|
|
6394
|
+
/**
|
|
6395
|
+
* 台帳の行がぶら下がる親(`oldParent`)を、いま要求している親(`newParent`)へ
|
|
6396
|
+
* 付け替えてよいか。**退役した親のときだけ**真 ── 生きているなら共有であって
|
|
6397
|
+
* 陳腐化ではないので、main と同じく 1 組の行集合に合流させる。
|
|
6398
|
+
* 深さ(`position`)が変わる付け替えはしない。行の `position` / `length` は鋳造時に
|
|
6399
|
+
* 確定していて、そこがずれると絶対アドレスの段数が壊れる(bind-component が 1 本の
|
|
6400
|
+
* 配列を 2 つの深さから展開する形が実際にある)。
|
|
6401
|
+
*/
|
|
6402
|
+
function canReparent(oldParent, newParent) {
|
|
6403
|
+
if (oldParent === newParent) {
|
|
6404
|
+
// 自分が展開した行集合(ここが圧倒的多数)。ルート直下どうし(両方 null)もここ。
|
|
6405
|
+
return false;
|
|
6406
|
+
}
|
|
6407
|
+
if (oldParent === null || newParent === null) {
|
|
6408
|
+
return false;
|
|
6409
|
+
}
|
|
6410
|
+
if (oldParent.position !== newParent.position) {
|
|
6411
|
+
return false;
|
|
6412
|
+
}
|
|
6413
|
+
return retiredListIndexes.has(oldParent) && !retiredListIndexes.has(newParent);
|
|
6414
|
+
}
|
|
6415
|
+
/**
|
|
6416
|
+
* 要求している親(`newParent`)が、退役した `home` の**行そのもの**か。
|
|
6417
|
+
* 行を戻す普通のやり方(新しい配列に同じ要素を並べ直す)では差分が ListIndex を作り直すので、
|
|
6418
|
+
* home の ListIndex は二度と生き返らない。同じリスト要素を同じ深さで表している生きた行が
|
|
6419
|
+
* 現れたら、それが戻ってきた home である。
|
|
6420
|
+
*/
|
|
6421
|
+
function isRestoredHome(home, oldParent, newParent) {
|
|
6422
|
+
if (home === null || newParent === null) {
|
|
6423
|
+
return false;
|
|
6424
|
+
}
|
|
6425
|
+
if (newParent === oldParent) {
|
|
6426
|
+
// すでにそこにある(=毎回 reparent して version を進めない)。
|
|
6427
|
+
return false;
|
|
6428
|
+
}
|
|
6429
|
+
if (!retiredListIndexes.has(home)) {
|
|
6430
|
+
// home が生きているなら「生き返った home」の分岐が扱う。
|
|
6431
|
+
return false;
|
|
6432
|
+
}
|
|
6433
|
+
if (retiredListIndexes.has(newParent) || newParent.position !== home.position) {
|
|
6434
|
+
return false;
|
|
6435
|
+
}
|
|
6436
|
+
return isSameListIndexValue(newParent, home);
|
|
6437
|
+
}
|
|
6438
|
+
/**
|
|
6439
|
+
* 行集合の親をどこへ向けるか。`null` なら何もしない。
|
|
6440
|
+
* 優先順位は **「生き返った home」>「戻ってきた home の行」>「陳腐化した親の付け替え」**。
|
|
6441
|
+
*/
|
|
6442
|
+
function getRepairTarget(first, parentListIndex) {
|
|
6443
|
+
const home = getHomeParentListIndex(first);
|
|
6444
|
+
const oldParent = first.parentListIndex;
|
|
6445
|
+
if (home !== null && home !== oldParent && !retiredListIndexes.has(home)) {
|
|
6446
|
+
return home;
|
|
6447
|
+
}
|
|
6448
|
+
if (isRestoredHome(home, oldParent, parentListIndex)) {
|
|
6449
|
+
return parentListIndex;
|
|
6450
|
+
}
|
|
6451
|
+
return canReparent(oldParent, parentListIndex) ? parentListIndex : null;
|
|
6452
|
+
}
|
|
6453
|
+
/**
|
|
6454
|
+
* 台帳を**引き当てるだけ**。行の親ポインタには触らない。
|
|
6455
|
+
* 観測(テスト・世代の後始末・`$scan` の drain が行の置き換わりを見る判定)と存在判定はこちらを使う。
|
|
6456
|
+
*/
|
|
6241
6457
|
function getListIndexesByList(list) {
|
|
6242
|
-
|
|
6458
|
+
const listIndexes = listIndexesByList.get(list);
|
|
6459
|
+
if (typeof listIndexes === "undefined") {
|
|
6460
|
+
return null;
|
|
6461
|
+
}
|
|
6462
|
+
return listIndexes;
|
|
6463
|
+
}
|
|
6464
|
+
/**
|
|
6465
|
+
* 台帳を引き当て、**必要なら親ポインタを修理して**返す(#256)。
|
|
6466
|
+
* 引き当てのついでに行を書き換えるので、観測目的では使わないこと ──
|
|
6467
|
+
* 修理が要るのは「その親の文脈で値を解決する」経路(差分・アドレス解決)だけ。
|
|
6468
|
+
*/
|
|
6469
|
+
function resolveListIndexesByList(list, parentListIndex) {
|
|
6470
|
+
const listIndexes = listIndexesByList.get(list);
|
|
6471
|
+
if (typeof listIndexes === "undefined") {
|
|
6472
|
+
return null;
|
|
6473
|
+
}
|
|
6474
|
+
const first = listIndexes[0];
|
|
6475
|
+
if (typeof first !== "undefined") {
|
|
6476
|
+
const target = getRepairTarget(first, parentListIndex);
|
|
6477
|
+
if (target !== null) {
|
|
6478
|
+
// 1 組の行集合は 1 つの親のもとにある、を保つ(差分が別の親の行を混ぜて
|
|
6479
|
+
// 作った集合も、ここで揃える)。
|
|
6480
|
+
for (const listIndex of listIndexes) {
|
|
6481
|
+
reparentListIndex(listIndex, target);
|
|
6482
|
+
}
|
|
6483
|
+
}
|
|
6484
|
+
}
|
|
6485
|
+
return listIndexes;
|
|
6243
6486
|
}
|
|
6244
6487
|
function setListIndexesByList(list, listIndexes) {
|
|
6245
6488
|
if (listIndexes === null) {
|
|
@@ -6292,12 +6535,16 @@ function isSameList(oldList, newList) {
|
|
|
6292
6535
|
* earlier diff in the same batch (two replacements in one microtask) may have
|
|
6293
6536
|
* moved shared indexes toward a list that never got applied, and a cache hit
|
|
6294
6537
|
* skips recomputation entirely — so every createListDiff return re-aligns.
|
|
6538
|
+
* 同じ走査で、行が表している要素も憶えさせる(#256)。行を戻す普通のやり方は
|
|
6539
|
+
* 配列を作り直すので ListIndex も作り直される ── 「同じ行が戻ってきた」と言えるのは
|
|
6540
|
+
* 行オブジェクトの identity ではなく、この値だけ。
|
|
6295
6541
|
*/
|
|
6296
|
-
function syncListIndexes(newIndexes) {
|
|
6542
|
+
function syncListIndexes(newIndexes, newList) {
|
|
6297
6543
|
for (let i = 0; i < newIndexes.length; i++) {
|
|
6298
6544
|
if (newIndexes[i].index !== i) {
|
|
6299
6545
|
newIndexes[i].index = i;
|
|
6300
6546
|
}
|
|
6547
|
+
setListIndexValue(newIndexes[i], newList[i]);
|
|
6301
6548
|
}
|
|
6302
6549
|
}
|
|
6303
6550
|
/**
|
|
@@ -6311,7 +6558,12 @@ function syncListIndexes(newIndexes) {
|
|
|
6311
6558
|
*/
|
|
6312
6559
|
function createListDiff(parentListIndex, rawOldList, rawNewList) {
|
|
6313
6560
|
const diff = computeListDiff(parentListIndex, rawOldList, rawNewList);
|
|
6314
|
-
syncListIndexes(diff.newIndexes);
|
|
6561
|
+
syncListIndexes(diff.newIndexes, (Array.isArray(rawNewList) && rawNewList.length > 0) ? rawNewList : EMPTY_LIST);
|
|
6562
|
+
// 捨てた行を退役、返した行を復活として記録する。台帳はこれを見て「共有」と「陳腐化」を
|
|
6563
|
+
// 分ける(#256)。両方を毎回の差分で付け直すので、消えない印は残らない。
|
|
6564
|
+
// deleteIndexSet と newIndexes は構造上交わらない。
|
|
6565
|
+
retireListIndexes(diff.deleteIndexSet);
|
|
6566
|
+
reviveListIndexes(diff.newIndexes);
|
|
6315
6567
|
return diff;
|
|
6316
6568
|
}
|
|
6317
6569
|
function computeListDiff(parentListIndex, rawOldList, rawNewList) {
|
|
@@ -6322,7 +6574,14 @@ function computeListDiff(parentListIndex, rawOldList, rawNewList) {
|
|
|
6322
6574
|
if (cachedDiff) {
|
|
6323
6575
|
return cachedDiff;
|
|
6324
6576
|
}
|
|
6325
|
-
|
|
6577
|
+
// 台帳は 1 本の配列につき行集合 1 組(listIndexesByList.ts)。親は「行がぶら下がる親が
|
|
6578
|
+
// 退役していたら、この親へ付け替える」ための差し替え先として渡す(#256)。
|
|
6579
|
+
const oldIndexes = resolveListIndexesByList(oldList, parentListIndex) || [];
|
|
6580
|
+
// 1 組の行集合の home は 1 つ(#256)。前の行集合を引き継ぐ差分で新しく鋳造する行は、
|
|
6581
|
+
// 鋳造時の親ではなくその行集合の home を継ぐ ── 行ごとに home が違う集合ができると、
|
|
6582
|
+
// 「持ち主が戻ってきたか」の判定が行の並び順で変わる。
|
|
6583
|
+
const homeParentListIndex = oldIndexes.length > 0 ?
|
|
6584
|
+
getHomeParentListIndex(oldIndexes[0]) : parentListIndex;
|
|
6326
6585
|
let retValue;
|
|
6327
6586
|
try {
|
|
6328
6587
|
// Early return for empty list
|
|
@@ -6336,12 +6595,12 @@ function computeListDiff(parentListIndex, rawOldList, rawNewList) {
|
|
|
6336
6595
|
};
|
|
6337
6596
|
}
|
|
6338
6597
|
// If old list was empty, create all new indexes
|
|
6339
|
-
let newIndexes =
|
|
6598
|
+
let newIndexes = resolveListIndexesByList(newList, parentListIndex);
|
|
6340
6599
|
if (oldList.length === 0) {
|
|
6341
6600
|
if (newIndexes === null) {
|
|
6342
6601
|
newIndexes = [];
|
|
6343
6602
|
for (let i = 0; i < newList.length; i++) {
|
|
6344
|
-
const newListIndex = createListIndex(parentListIndex, i);
|
|
6603
|
+
const newListIndex = createListIndex(parentListIndex, i, homeParentListIndex);
|
|
6345
6604
|
newIndexes.push(newListIndex);
|
|
6346
6605
|
}
|
|
6347
6606
|
}
|
|
@@ -6388,7 +6647,7 @@ function computeListDiff(parentListIndex, rawOldList, rawNewList) {
|
|
|
6388
6647
|
const oldIndex = existingIndexes && existingIndexes.length > 0 ? existingIndexes.shift() : undefined;
|
|
6389
6648
|
if (typeof oldIndex === "undefined") {
|
|
6390
6649
|
// New element
|
|
6391
|
-
const newListIndex = createListIndex(parentListIndex, i);
|
|
6650
|
+
const newListIndex = createListIndex(parentListIndex, i, homeParentListIndex);
|
|
6392
6651
|
newIndexes.push(newListIndex);
|
|
6393
6652
|
addIndexSet.add(newListIndex);
|
|
6394
6653
|
}
|
|
@@ -8230,11 +8489,65 @@ function getVolumeUpdatedCallbacks(stateElement) {
|
|
|
8230
8489
|
return volumeUpdatedCallbacksByRoot.get(stateElement) ?? NO_VOLUME_UPDATED_CALLBACKS;
|
|
8231
8490
|
}
|
|
8232
8491
|
const pendingVolumesByRootNode = new WeakMap();
|
|
8492
|
+
/**
|
|
8493
|
+
* ルートの state 要素が初期化に失敗した rootNode(#257)。ルート登録(setStateElement)は
|
|
8494
|
+
* 二度と起きないので `drainPendingVolumes` も呼ばれず、保留中のボリュームの
|
|
8495
|
+
* `onGrafted` が走らないまま initializePromise / connectedCallbackPromise が永久に
|
|
8496
|
+
* 未解決になる(ルートの診断だけが出て、同じページのボリュームは無言で消える)。
|
|
8497
|
+
* 「ルートは来ない」と確定した時点で保留分を孤児として着地させ、以後に届く保留要求も
|
|
8498
|
+
* 同じ着地へ合流させる。D11 の「ルート無し」報告はここには出ない — 検査(State.ts の
|
|
8499
|
+
* reportVolumeWithoutRoot)は**要素の存在**で見るので、落ちたルート要素が居る限り黙る。
|
|
8500
|
+
*
|
|
8501
|
+
* 印は**落ちた要素がこの rootNode に居る間だけ**有効(`clearFailedRootNode`)。持続させると、
|
|
8502
|
+
* この PR が案内する復旧(壊れた要素を取り除いて作り直す)と矛盾する: 外してから修正版を
|
|
8503
|
+
* 接続するまでの窓で接続したボリュームが即座に孤児化し、正しいルートが来ても採用されない。
|
|
8504
|
+
*/
|
|
8505
|
+
const failedRootNodes = new WeakSet();
|
|
8506
|
+
/** 接ぎ木先を失ったボリュームの着地(graftIsolated の失敗と同じ形 — 1 件 1 報告 + finish(null))。 */
|
|
8507
|
+
function orphanPendingVolume(request) {
|
|
8508
|
+
console.error(`[@wcstack/state] volume "${request.mountPath}" was not grafted — the root state element ` +
|
|
8509
|
+
`on this root node failed to initialize (its own diagnostic is reported separately). ` +
|
|
8510
|
+
`The volume is not at fault: fix the root <wcs-state>.`);
|
|
8511
|
+
request.onGrafted(null);
|
|
8512
|
+
}
|
|
8513
|
+
/**
|
|
8514
|
+
* 失敗の印を落とす(#257)。呼び手は State の `disconnectedCallback` ただ 1 つで、
|
|
8515
|
+
* **初期化に失敗した当の要素**が剥がされたときだけ呼ぶ — 落ちたルートが DOM から
|
|
8516
|
+
* 消えた時点で「このルートノードにルートは来ない」は成り立たなくなる(作者は
|
|
8517
|
+
* 取り除いて作り直す)。
|
|
8518
|
+
*
|
|
8519
|
+
* 呼び手を本人に限るのは第 3 ラウンドの修正: 「初期化前に剥がされた要素」全部で
|
|
8520
|
+
* 落としていたため、同じ rootNode の別要素(登録されなかった 2 本目・行プールの
|
|
8521
|
+
* 張り直し・ロード中の DOM 移動)の切断で印が消え、以後のボリュームが孤児報告を
|
|
8522
|
+
* 受けられず永久保留へ戻っていた。
|
|
8523
|
+
*/
|
|
8524
|
+
function clearFailedRootNode(rootNode) {
|
|
8525
|
+
failedRootNodes.delete(rootNode);
|
|
8526
|
+
}
|
|
8527
|
+
/** ルートの初期化失敗を確定し、保留中のボリュームを孤児として着地させる(State の _failInitializeLoudly が唯一の呼び手)。 */
|
|
8528
|
+
function failPendingVolumes(rootNode) {
|
|
8529
|
+
failedRootNodes.add(rootNode);
|
|
8530
|
+
const pending = pendingVolumesByRootNode.get(rootNode);
|
|
8531
|
+
if (typeof pending === "undefined") {
|
|
8532
|
+
// 保留はまだ無い(ボリュームのロードのほうが遅い形)— 下の queuePendingVolume が拾う
|
|
8533
|
+
return;
|
|
8534
|
+
}
|
|
8535
|
+
pendingVolumesByRootNode.delete(rootNode);
|
|
8536
|
+
for (const request of pending) {
|
|
8537
|
+
orphanPendingVolume(request);
|
|
8538
|
+
}
|
|
8539
|
+
}
|
|
8233
8540
|
let graftHandler = null;
|
|
8234
8541
|
function setVolumeGraftHandler(handler) {
|
|
8235
8542
|
graftHandler = handler;
|
|
8236
8543
|
}
|
|
8237
8544
|
function queuePendingVolume(rootNode, request) {
|
|
8545
|
+
if (failedRootNodes.has(rootNode)) {
|
|
8546
|
+
// ルートが落ちた後に届いた保留要求(ボリュームのロードのほうが遅い形)。
|
|
8547
|
+
// 積んでも引き取り手は永久に来ない
|
|
8548
|
+
orphanPendingVolume(request);
|
|
8549
|
+
return;
|
|
8550
|
+
}
|
|
8238
8551
|
let pending = pendingVolumesByRootNode.get(rootNode);
|
|
8239
8552
|
if (typeof pending === "undefined") {
|
|
8240
8553
|
pending = [];
|
|
@@ -8244,6 +8557,9 @@ function queuePendingVolume(rootNode, request) {
|
|
|
8244
8557
|
}
|
|
8245
8558
|
/** ルート登録時に保留中のボリュームを接ぎ木する(stateElementByName から呼ばれる)。 */
|
|
8246
8559
|
function drainPendingVolumes(rootNode, rootStateElement) {
|
|
8560
|
+
// ルートが成立した = 失敗の印は無効(落ちたルートを外して正しいルートを接続し直した
|
|
8561
|
+
// 形。WeakSet.delete は未登録でも安全なので分岐は要らない)
|
|
8562
|
+
failedRootNodes.delete(rootNode);
|
|
8247
8563
|
const pending = pendingVolumesByRootNode.get(rootNode);
|
|
8248
8564
|
if (typeof pending === "undefined" || pending.length === 0 || graftHandler === null) {
|
|
8249
8565
|
return;
|
|
@@ -8395,10 +8711,12 @@ function resolvePathExistence(target, path, declaredPaths) {
|
|
|
8395
8711
|
const DIAGNOSTIC_CODE = {
|
|
8396
8712
|
binding: "wcs/binding-path-missing",
|
|
8397
8713
|
watch: "wcs/watch-path-missing",
|
|
8714
|
+
scan: "wcs/scan-path-missing",
|
|
8398
8715
|
};
|
|
8399
8716
|
const SUBJECT = {
|
|
8400
8717
|
binding: "Bound path",
|
|
8401
8718
|
watch: "$watch path",
|
|
8719
|
+
scan: "$scan path",
|
|
8402
8720
|
};
|
|
8403
8721
|
/**
|
|
8404
8722
|
* ルート直下(単一セグメント)のパスが state に無いときのエラーメッセージ。
|
|
@@ -9547,7 +9865,7 @@ async function buildBindings(root) {
|
|
|
9547
9865
|
}
|
|
9548
9866
|
}
|
|
9549
9867
|
|
|
9550
|
-
var version = "2.
|
|
9868
|
+
var version = "2.4.0";
|
|
9551
9869
|
var pkg = {
|
|
9552
9870
|
version: version};
|
|
9553
9871
|
|
|
@@ -10508,6 +10826,26 @@ function getStateElement(rootNode) {
|
|
|
10508
10826
|
function getBindingsReady(rootNode) {
|
|
10509
10827
|
return bindingsReadyByNode.get(rootNode) ?? Promise.resolve();
|
|
10510
10828
|
}
|
|
10829
|
+
/**
|
|
10830
|
+
* この rootNode ではバインド構築が起きない、と確定する(#257)。
|
|
10831
|
+
*
|
|
10832
|
+
* ルートの state 要素が初期化に失敗すると `setStateElement` に到達しないので、
|
|
10833
|
+
* 台帳はエントリが無いまま残る。上の既定(未登録 = 即時解決)は「まだ登録されて
|
|
10834
|
+
* いない」と「バインドを張り終えた」を区別しないため、@wcstack/server の
|
|
10835
|
+
* `waitForReady` は**バインディングが 1 本も無いページ**を ready と報告してしまう。
|
|
10836
|
+
* 失敗した rootNode には reject 済みの ready を置き、待ち手へ同じ診断を届ける。
|
|
10837
|
+
*
|
|
10838
|
+
* 呼び手は State の `_failInitializeLoudly` ただ 1 つで、そちらが「この rootNode に
|
|
10839
|
+
* 生きたルートが居ない」ことを確かめてから呼ぶ(2 本目の `<wcs-state>` が落ちても
|
|
10840
|
+
* 1 本目の ready は壊さない)。
|
|
10841
|
+
*/
|
|
10842
|
+
function markBindingsUnavailable(rootNode, error) {
|
|
10843
|
+
const failed = Promise.reject(error);
|
|
10844
|
+
// reject と同じティックで handled を立てる(getBindingsReady を誰も呼ばない
|
|
10845
|
+
// ページで unhandled rejection にしないため)。await した側は従来どおり受け取る
|
|
10846
|
+
failed.catch(() => undefined);
|
|
10847
|
+
bindingsReadyByNode.set(rootNode, failed);
|
|
10848
|
+
}
|
|
10511
10849
|
const bindingsBuiltRoots = new WeakSet();
|
|
10512
10850
|
/**
|
|
10513
10851
|
* マウントされたスコープ(コンポーネントの ShadowRoot)に、**親の** state element を
|
|
@@ -10576,6 +10914,16 @@ function setStateElement(rootNode, element) {
|
|
|
10576
10914
|
}
|
|
10577
10915
|
else {
|
|
10578
10916
|
// 登録の場合
|
|
10917
|
+
if (existing === element) {
|
|
10918
|
+
// 同じ要素の再登録は冪等(setStateElementAlias と同じ規範)。ロード完了前の
|
|
10919
|
+
// remove → append(DOM の移動・行プールの張り直し・shadow の組み直し)は
|
|
10920
|
+
// `_initialize` を 2 本同時に走らせるので、後から登録に来たほうが**自分自身**を
|
|
10921
|
+
// 「2 本目の <wcs-state>」と誤診する。DOM の移動は正常な操作であり、拒否すると
|
|
10922
|
+
// 健全なページの connectedCallbackPromise が reject される(#257)。
|
|
10923
|
+
// 切断時に登録を解除する案では直らない — 実測で、切断の時点ではまだ何も
|
|
10924
|
+
// 登録されていない(登録は進行中の `_initialize` の続きで起きる)。
|
|
10925
|
+
return;
|
|
10926
|
+
}
|
|
10579
10927
|
if (existing === undefined) {
|
|
10580
10928
|
// 初めてルートノードに登録する場合
|
|
10581
10929
|
// enable-ssr 属性があり、サーバーサイドでない場合はハイドレーション
|
|
@@ -10639,8 +10987,14 @@ function setStateElement(rootNode, element) {
|
|
|
10639
10987
|
if (existing !== undefined) {
|
|
10640
10988
|
// v2 は 1 rootNode 1 ツリー。2 つ目の <wcs-state> は設定エラー — 追加の状態は
|
|
10641
10989
|
// マウント(mount= / ホスト配線の bind-component)でツリーに載せる
|
|
10990
|
+
// 文言は実測のゾンビの姿に合わせる(#257 第 3 ラウンド): 2 本目は state を
|
|
10991
|
+
// 組み上げたまま**登録されない**。データもトークンも $on の購読も生きているのに
|
|
10992
|
+
// 誰もバインドしておらず、_initialized が false なので切断時の後始末も走らない
|
|
10642
10993
|
raiseError(`A state tree is already registered on this root — one <wcs-state> per root in v2. ` +
|
|
10643
|
-
`
|
|
10994
|
+
`This second element stays unregistered: it keeps the state it loaded and its ` +
|
|
10995
|
+
`declarations stay live, but nothing binds to it and disconnecting it runs no ` +
|
|
10996
|
+
`cleanup — remove it. Mount additional states onto the tree instead: ` +
|
|
10997
|
+
`<wcs-state mount="...">.`);
|
|
10644
10998
|
}
|
|
10645
10999
|
stateElementByNode.set(rootNode, element);
|
|
10646
11000
|
liveStateElements.add(element);
|
|
@@ -10701,12 +11055,233 @@ function consumeWatchChainDepth() {
|
|
|
10701
11055
|
return depth;
|
|
10702
11056
|
}
|
|
10703
11057
|
|
|
11058
|
+
/**
|
|
11059
|
+
* scan/scanRegistry.ts
|
|
11060
|
+
*
|
|
11061
|
+
* `$scan` の registry(docs/state-scan-design.md §2)。
|
|
11062
|
+
*
|
|
11063
|
+
* `$watch` の registry とは別台帳にする。watch の registry はパスにつき entry 1 個
|
|
11064
|
+
* (`Map<string, IWatchEntry>`)なので、同じパスを `$watch` と `$scan` が並んで
|
|
11065
|
+
* 見る形・同じ `from` を 2 つの scan が畳む形を載せられない。
|
|
11066
|
+
*
|
|
11067
|
+
* 寿命は `$watch` と揃える: `_state` 再セットで作り直し、切断では消さない
|
|
11068
|
+
* (発火対象から外れるのは watch runtime の active 集合側)。
|
|
11069
|
+
*/
|
|
11070
|
+
const registryByStateElement$3 = new WeakMap();
|
|
11071
|
+
/**
|
|
11072
|
+
* drain 側の粗いゲート: 発火対象(watch runtime の active 集合)に居て、`from` か `resetOn` を持つ state。
|
|
11073
|
+
*
|
|
11074
|
+
* 空のあいだ watch runtime は scan の収集ループに入らない = `$scan` を使っていない画面の drain に、
|
|
11075
|
+
* バッチのアドレスごとの registry 引きを載せない。registry の有無ではなく active 集合への出入り
|
|
11076
|
+
* (watch/watchRegistry.ts の addActiveWatchStateElement / deactivateWatch / clearWatchRegistry)で
|
|
11077
|
+
* 数えるので、`$scan` を持つ state を DOM から外せば(SPA のページ差し替え)ゲートは閉じ、
|
|
11078
|
+
* 再接続(startWatch)で開き直す。strong Set でも、切断と再セットで必ず外れるので GC を妨げない
|
|
11079
|
+
* (active 集合と同じ不変条件)。
|
|
11080
|
+
*/
|
|
11081
|
+
const drainActive = new Set();
|
|
11082
|
+
/**
|
|
11083
|
+
* enqueue 側のゲート: 発火対象に居て、`resetOn` を持つ `on` scan がある state(scan/eventReset.ts)。
|
|
11084
|
+
* 空のあいだ updater の enqueue は整数比較 1 回で抜ける。数え方は drainActive と同じ。
|
|
11085
|
+
*/
|
|
11086
|
+
const eventResetActive = new Set();
|
|
11087
|
+
function hasDrainWork(registry) {
|
|
11088
|
+
return registry.byFromPath.size > 0 || registry.byResetPath.size > 0;
|
|
11089
|
+
}
|
|
11090
|
+
function hasEventResetWork(registry) {
|
|
11091
|
+
return registry.eventResetByPath.size > 0;
|
|
11092
|
+
}
|
|
11093
|
+
function pushEntry(map, key, entry) {
|
|
11094
|
+
const list = map.get(key);
|
|
11095
|
+
if (typeof list === "undefined") {
|
|
11096
|
+
map.set(key, [entry]);
|
|
11097
|
+
}
|
|
11098
|
+
else {
|
|
11099
|
+
list.push(entry);
|
|
11100
|
+
}
|
|
11101
|
+
}
|
|
11102
|
+
/** registry を置換登録する(`_state` セッターから)。 */
|
|
11103
|
+
function setScanRegistry(stateElement, entries) {
|
|
11104
|
+
clearScanRegistry(stateElement);
|
|
11105
|
+
const byFromPath = new Map();
|
|
11106
|
+
const byResetPath = new Map();
|
|
11107
|
+
const eventResetByPath = new Map();
|
|
11108
|
+
for (const entry of entries) {
|
|
11109
|
+
if (entry.source.kind === "path") {
|
|
11110
|
+
pushEntry(byFromPath, entry.source.path, entry);
|
|
11111
|
+
}
|
|
11112
|
+
for (const path of entry.resetOn) {
|
|
11113
|
+
pushEntry(byResetPath, path, entry);
|
|
11114
|
+
if (entry.source.kind === "event") {
|
|
11115
|
+
pushEntry(eventResetByPath, path, entry);
|
|
11116
|
+
}
|
|
11117
|
+
}
|
|
11118
|
+
}
|
|
11119
|
+
const registry = { entries: new Set(entries), byFromPath, byResetPath, eventResetByPath };
|
|
11120
|
+
registryByStateElement$3.set(stateElement, registry);
|
|
11121
|
+
return registry;
|
|
11122
|
+
}
|
|
11123
|
+
function getScanRegistry(stateElement) {
|
|
11124
|
+
return registryByStateElement$3.get(stateElement);
|
|
11125
|
+
}
|
|
11126
|
+
/**
|
|
11127
|
+
* registry を削除する(`_state` 再セットで作り直す前)。ゲートからも外す — 同じ再セットの
|
|
11128
|
+
* startWatch が新しい registry で数え直す(`_state` セッターは registry を作った後に
|
|
11129
|
+
* clearWatchRegistry → startWatch を通る)。
|
|
11130
|
+
*/
|
|
11131
|
+
function clearScanRegistry(stateElement) {
|
|
11132
|
+
deactivateScanGates(stateElement);
|
|
11133
|
+
registryByStateElement$3.delete(stateElement);
|
|
11134
|
+
}
|
|
11135
|
+
/** 発火対象に載った(watch/watchRegistry.ts の addActiveWatchStateElement 専用)。冪等。 */
|
|
11136
|
+
function activateScanGates(stateElement) {
|
|
11137
|
+
const registry = registryByStateElement$3.get(stateElement);
|
|
11138
|
+
if (typeof registry === "undefined") {
|
|
11139
|
+
return;
|
|
11140
|
+
}
|
|
11141
|
+
if (hasDrainWork(registry)) {
|
|
11142
|
+
drainActive.add(stateElement);
|
|
11143
|
+
}
|
|
11144
|
+
if (hasEventResetWork(registry)) {
|
|
11145
|
+
eventResetActive.add(stateElement);
|
|
11146
|
+
}
|
|
11147
|
+
}
|
|
11148
|
+
/** 発火対象から外れた(切断・再セット)。 */
|
|
11149
|
+
function deactivateScanGates(stateElement) {
|
|
11150
|
+
drainActive.delete(stateElement);
|
|
11151
|
+
eventResetActive.delete(stateElement);
|
|
11152
|
+
}
|
|
11153
|
+
/**
|
|
11154
|
+
* drain で発火すべき scan(`from` か `resetOn`)を持つか。
|
|
11155
|
+
* `startWatch` が発火対象集合へ載せる判定に使う(`on` だけの scan は drain を要らない)。
|
|
11156
|
+
*/
|
|
11157
|
+
function hasScanDrainWork(stateElement) {
|
|
11158
|
+
const registry = registryByStateElement$3.get(stateElement);
|
|
11159
|
+
return typeof registry !== "undefined" && hasDrainWork(registry);
|
|
11160
|
+
}
|
|
11161
|
+
/** 発火対象に居て、`resetOn` を持つ `on` scan がある state か(enqueue の保留判定)。 */
|
|
11162
|
+
function hasActiveScanEventReset(stateElement) {
|
|
11163
|
+
return eventResetActive.has(stateElement);
|
|
11164
|
+
}
|
|
11165
|
+
function getScanDrainGateCount() {
|
|
11166
|
+
return drainActive.size;
|
|
11167
|
+
}
|
|
11168
|
+
function getScanEventResetGateCount() {
|
|
11169
|
+
return eventResetActive.size;
|
|
11170
|
+
}
|
|
11171
|
+
|
|
11172
|
+
/**
|
|
11173
|
+
* scan/eventReset.ts
|
|
11174
|
+
*
|
|
11175
|
+
* `on` scan の `resetOn` を「書き込みの時点」で効かせる台帳(docs/state-scan-design.md D6)。
|
|
11176
|
+
*
|
|
11177
|
+
* `on` の fold は出来事のその場で同期に書く。一方 `resetOn` のパスはバッチに載って drain の
|
|
11178
|
+
* 終わりにしか見えない。drain で reset するだけだと、reset の書き込みから drain までに起きた
|
|
11179
|
+
* 出来事 — その書き込みの binding 適用で要素が同期に dispatch したものを含む — まで消える。
|
|
11180
|
+
*
|
|
11181
|
+
* そこで reset の要求を enqueue の時点で entry に保留する。
|
|
11182
|
+
* - 保留中に来た出来事の fold は `initial` から畳み、出力の書き込みが通ったら保留を消す
|
|
11183
|
+
* (fold の throw・thenable・書き込みの throw なら残す)。
|
|
11184
|
+
* - 保留が残ったまま drain に来たら、scan runtime が出力を `initial` に戻して保留を消す。
|
|
11185
|
+
* これで「書き込みより後の出来事は reset 後の出力に畳まれる」順序になる。
|
|
11186
|
+
*
|
|
11187
|
+
* 保留は「その書き込みが載ったバッチの drain で initial に戻す」予約なので、寿命をそのバッチに
|
|
11188
|
+
* 揃える。drain がそのバッチの scan を発火しない(書き込みの後・drain の前に切断された・連鎖深さの
|
|
11189
|
+
* 上限で打ち切った・先行 fold が同期に切断や再セットをした)なら保留を捨てる。残すと、後の無関係な
|
|
11190
|
+
* 出来事が突然 `initial` から畳み始める。同じ条件の `from` の scan は reset しない(発火しない)ので、
|
|
11191
|
+
* それに揃える。保留は entry ごとの 1 bit でどのバッチの書き込みかを持たないので、同じ `resetOn` の
|
|
11192
|
+
* パスが次のバッチ向けに既に積まれていれば捨てない — その保留は次のバッチの drain が使う。
|
|
11193
|
+
*
|
|
11194
|
+
* updater(enqueue 側)から呼ばれるので updater を import しない(watch/chainDepth.ts と同じ理由)。
|
|
11195
|
+
*/
|
|
11196
|
+
const pendingResets = new WeakSet();
|
|
11197
|
+
/**
|
|
11198
|
+
* 保留中の entry の数。捨てる走査(discardPendingScanResets)のゲート。保留は enqueue の直後の drain で
|
|
11199
|
+
* 使うか捨てるか次のバッチへ持ち越すので、ふつうは 0。enqueue のゲート(発火対象の state)と別に持つのは、
|
|
11200
|
+
* 書き込みの後・drain の前に切断された state がゲートから外れても、その保留は drain で捨てるため。
|
|
11201
|
+
*/
|
|
11202
|
+
let pendingResetCount = 0;
|
|
11203
|
+
/** このアドレスを `resetOn` に持つ `on` scan(無ければ undefined) */
|
|
11204
|
+
function eventResetEntriesAt(absAddress) {
|
|
11205
|
+
return getScanRegistry(absAddress.absolutePathInfo.stateElement)
|
|
11206
|
+
?.eventResetByPath.get(absAddress.absolutePathInfo.pathInfo.path);
|
|
11207
|
+
}
|
|
11208
|
+
/**
|
|
11209
|
+
* 書き込みの enqueue を記録する(updater 専用)。
|
|
11210
|
+
* `resetOn` を持つ `on` scan が発火対象の state に 1 つも無ければ(そうした state を DOM から外した後を含む)、
|
|
11211
|
+
* 整数比較 1 回で抜ける。
|
|
11212
|
+
*/
|
|
11213
|
+
function noteEnqueueForScanReset(absAddress) {
|
|
11214
|
+
if (getScanEventResetGateCount() === 0) {
|
|
11215
|
+
return;
|
|
11216
|
+
}
|
|
11217
|
+
// 発火対象でない state(切断中・初期化中・SSR)の書き込みは reset しない — drain 側と同じ扱い
|
|
11218
|
+
if (!hasActiveScanEventReset(absAddress.absolutePathInfo.stateElement)) {
|
|
11219
|
+
return;
|
|
11220
|
+
}
|
|
11221
|
+
const entries = eventResetEntriesAt(absAddress);
|
|
11222
|
+
if (typeof entries === "undefined") {
|
|
11223
|
+
return;
|
|
11224
|
+
}
|
|
11225
|
+
for (const entry of entries) {
|
|
11226
|
+
markPendingScanReset(entry);
|
|
11227
|
+
}
|
|
11228
|
+
}
|
|
11229
|
+
/**
|
|
11230
|
+
* 保留を立てる。enqueue の記録(上)と、`_state` 再セットで旧宣言の保留を同じ出力の `on` scan へ
|
|
11231
|
+
* 引き継ぐとき(processScanDeclaration.ts の registerScans)だけが呼ぶ。
|
|
11232
|
+
*/
|
|
11233
|
+
function markPendingScanReset(entry) {
|
|
11234
|
+
if (!pendingResets.has(entry)) {
|
|
11235
|
+
pendingResets.add(entry);
|
|
11236
|
+
pendingResetCount++;
|
|
11237
|
+
}
|
|
11238
|
+
}
|
|
11239
|
+
function hasPendingScanReset(entry) {
|
|
11240
|
+
return pendingResets.has(entry);
|
|
11241
|
+
}
|
|
11242
|
+
/** 保留があれば消して true を返す。 */
|
|
11243
|
+
function consumePendingScanReset(entry) {
|
|
11244
|
+
if (!pendingResets.delete(entry)) {
|
|
11245
|
+
return false;
|
|
11246
|
+
}
|
|
11247
|
+
pendingResetCount--;
|
|
11248
|
+
return true;
|
|
11249
|
+
}
|
|
11250
|
+
/**
|
|
11251
|
+
* drain がバッチの scan を発火しないとき、そのバッチが載せた保留を捨てる。
|
|
11252
|
+
* `firing` はこの drain で scan を発火する state の集合。null はバッチ全体を発火しない
|
|
11253
|
+
* (連鎖深さの上限)。`isQueued` は「次のバッチ向けに、この state のこのパスが積まれているか」
|
|
11254
|
+
* (updater を import しないので呼び出し側から受け取る)。
|
|
11255
|
+
*/
|
|
11256
|
+
function discardPendingScanResets(batch, firing, isQueued) {
|
|
11257
|
+
if (pendingResetCount === 0) {
|
|
11258
|
+
return;
|
|
11259
|
+
}
|
|
11260
|
+
for (const absAddress of batch) {
|
|
11261
|
+
const stateElement = absAddress.absolutePathInfo.stateElement;
|
|
11262
|
+
if (firing?.has(stateElement) === true) {
|
|
11263
|
+
continue;
|
|
11264
|
+
}
|
|
11265
|
+
const entries = eventResetEntriesAt(absAddress);
|
|
11266
|
+
if (typeof entries === "undefined") {
|
|
11267
|
+
continue;
|
|
11268
|
+
}
|
|
11269
|
+
for (const entry of entries) {
|
|
11270
|
+
if (!entry.resetOn.some((path) => isQueued(stateElement, path))) {
|
|
11271
|
+
consumePendingScanReset(entry);
|
|
11272
|
+
}
|
|
11273
|
+
}
|
|
11274
|
+
}
|
|
11275
|
+
}
|
|
11276
|
+
|
|
10704
11277
|
const updateBatchListeners = [];
|
|
10705
11278
|
/**
|
|
10706
11279
|
* drain 終了リスナーを登録する。
|
|
10707
11280
|
*
|
|
10708
11281
|
* `priority` の昇順に呼ばれる(同値は登録順)。機構間の実行順序
|
|
10709
|
-
* (`$watch` → `$streams` restart
|
|
11282
|
+
* (`$scan` → `$watch` → `$streams` restart。前の 2 つは同じ watch リスナーの中の順序で、
|
|
11283
|
+
* `$scan` は畳んで書いてから `$watch` を発火する —
|
|
11284
|
+
* docs/state-watch-hook-design.md §3-2 層 1・docs/state-scan-design.md D11)は
|
|
10710
11285
|
* この優先度で固定する — import 順に順序を持たせると、無関係な import 整理で
|
|
10711
11286
|
* 静かに壊れるため。定数は define.ts の `*_LISTENER_PRIORITY` を使うこと。
|
|
10712
11287
|
*/
|
|
@@ -10760,6 +11335,9 @@ class Updater {
|
|
|
10760
11335
|
// `$watch` ハンドラ実行中の書き込みだけを連鎖としてマークする(watch/chainDepth.ts)。
|
|
10761
11336
|
// ハンドラ実行中でなければ即 return する葉モジュール呼び出し 1 個のコスト。
|
|
10762
11337
|
noteEnqueueForWatchChain();
|
|
11338
|
+
// `on` scan の `resetOn` を書き込みの時点で保留する(scan/eventReset.ts)。
|
|
11339
|
+
// 該当する宣言がページに無ければ整数比較 1 回で抜ける。
|
|
11340
|
+
noteEnqueueForScanReset(absoluteAddress);
|
|
10763
11341
|
const requireStartProcess = this._queueUpdateRecords.length === 0;
|
|
10764
11342
|
this._queueUpdateRecords.push({ absoluteAddress, context });
|
|
10765
11343
|
if (requireStartProcess) {
|
|
@@ -10773,6 +11351,15 @@ class Updater {
|
|
|
10773
11351
|
});
|
|
10774
11352
|
}
|
|
10775
11353
|
}
|
|
11354
|
+
/**
|
|
11355
|
+
* まだ drain されていない書き込み(次のバッチ)に、この state のこのパスがあるか。
|
|
11356
|
+
* drain の最中のキューは次のバッチの分だけになっている(_applyChange の前に差し替える)。
|
|
11357
|
+
* `on` scan の保留 reset を、発火しない drain で捨ててよいかの判定に使う(scan/eventReset.ts)。
|
|
11358
|
+
*/
|
|
11359
|
+
hasQueuedPath(stateElement, path) {
|
|
11360
|
+
return this._queueUpdateRecords.some((record) => record.absoluteAddress.absolutePathInfo.stateElement === stateElement
|
|
11361
|
+
&& record.absoluteAddress.absolutePathInfo.pathInfo.path === path);
|
|
11362
|
+
}
|
|
10776
11363
|
// テスト用に公開
|
|
10777
11364
|
testApplyChange(absoluteAddresses, contexts) {
|
|
10778
11365
|
this._applyChange(absoluteAddresses.map((absoluteAddress, index) => ({
|
|
@@ -11598,9 +12185,6 @@ function getOrCreateCommandToken(stateElement, name) {
|
|
|
11598
12185
|
}
|
|
11599
12186
|
return token;
|
|
11600
12187
|
}
|
|
11601
|
-
function clearCommandTokenRegistry(stateElement) {
|
|
11602
|
-
registryByStateElement$2.delete(stateElement);
|
|
11603
|
-
}
|
|
11604
12188
|
|
|
11605
12189
|
/**
|
|
11606
12190
|
* `state.$command` でアクセスされる command token の namespace proxy を提供する。
|
|
@@ -12911,7 +13495,11 @@ class RecursionRegistry {
|
|
|
12911
13495
|
* 再セット時、`getStateInfo` の再収集より**前**に呼ぶ)。実体は generation.ts。
|
|
12912
13496
|
*/
|
|
12913
13497
|
forgetGenerated(stateElement, previousState) {
|
|
12914
|
-
|
|
13498
|
+
const generatedPaths = new Set(this._accessors.keys());
|
|
13499
|
+
forgetGeneration(stateElement, previousState, generatedPaths);
|
|
13500
|
+
// 忘れた具体パスを返す。`_state` のセッタは経路情報を作り直すときにこれを除く —
|
|
13501
|
+
// この世代ではまだ実体化されていないので、辺だけ張り直してはならない。
|
|
13502
|
+
return generatedPaths;
|
|
12915
13503
|
}
|
|
12916
13504
|
/**
|
|
12917
13505
|
* `**` 接尾辞の深さ `depth` の具体パス(`concretePathAt` の記憶付き版)。
|
|
@@ -13030,6 +13618,69 @@ function clearStreamNamespace(stateElement) {
|
|
|
13030
13618
|
errorNamespaceByStateElement.delete(stateElement);
|
|
13031
13619
|
}
|
|
13032
13620
|
|
|
13621
|
+
/**
|
|
13622
|
+
* scan/scanFeedback.ts
|
|
13623
|
+
*
|
|
13624
|
+
* 自己ループの柵(docs/state-scan-design.md D9 / §2-3)。
|
|
13625
|
+
*
|
|
13626
|
+
* `from` の根が `$streams` 名である scan について、その stream の `args` 依存に
|
|
13627
|
+
* scan 出力から到達できるパスが含まれていたら raise する。
|
|
13628
|
+
*
|
|
13629
|
+
* $scan.feed { from: "pageResult" }
|
|
13630
|
+
* get page() { return Math.floor(this.feed.items.length / this.pageSize) + 1; }
|
|
13631
|
+
* $streams.pageResult { args: (s) => ({ page: s.page }) }
|
|
13632
|
+
*
|
|
13633
|
+
* は、ページが着地するたびに `feed` → `page` → restart と進み、sentinel を経由せずに
|
|
13634
|
+
* 全ページを読み続ける(冪等キーが無ければ同じページを無限に取り直す)。
|
|
13635
|
+
*
|
|
13636
|
+
* 辿る辺は書き込みが届く先そのもの:
|
|
13637
|
+
* - `staticDependency`(親 → 子)と `dynamicDependency`(読まれたパス → それを読む getter)。
|
|
13638
|
+
* 書き込みの伝播(`walkDependency`)と同じグラフ。
|
|
13639
|
+
* - 到達したパスを `from` に持つ別の scan の出力。依存グラフには載らない辺だが、その scan が
|
|
13640
|
+
* 次のバッチで畳んで書くので、`feed` → `feed2`(`from: "feed"`)→ `page` の連鎖も同じループになる。
|
|
13641
|
+
*/
|
|
13642
|
+
const NO_EDGES = Object.freeze([]);
|
|
13643
|
+
const NO_SCANS = Object.freeze([]);
|
|
13644
|
+
function collectReachablePaths(stateElement, registry, start) {
|
|
13645
|
+
const reachable = new Set([start]);
|
|
13646
|
+
const queue = [start];
|
|
13647
|
+
while (queue.length > 0) {
|
|
13648
|
+
const path = queue.pop();
|
|
13649
|
+
const children = stateElement.staticDependency.get(path) ?? NO_EDGES;
|
|
13650
|
+
const dependents = stateElement.dynamicDependency.get(path) ?? NO_EDGES;
|
|
13651
|
+
const downstream = registry.byFromPath.get(path) ?? NO_SCANS;
|
|
13652
|
+
for (const next of [...children, ...dependents, ...downstream.map((scan) => scan.name)]) {
|
|
13653
|
+
if (!reachable.has(next)) {
|
|
13654
|
+
reachable.add(next);
|
|
13655
|
+
queue.push(next);
|
|
13656
|
+
}
|
|
13657
|
+
}
|
|
13658
|
+
}
|
|
13659
|
+
return reachable;
|
|
13660
|
+
}
|
|
13661
|
+
/** stream の起動・restart(`traceArgs` の直後)で呼ぶ。 */
|
|
13662
|
+
function assertNoScanFeedback(stateElement, streamEntry) {
|
|
13663
|
+
const registry = getScanRegistry(stateElement);
|
|
13664
|
+
if (typeof registry === "undefined" || streamEntry.depAddresses.size === 0) {
|
|
13665
|
+
return;
|
|
13666
|
+
}
|
|
13667
|
+
for (const scan of registry.entries) {
|
|
13668
|
+
if (scan.source.kind !== "path" || scan.source.pathInfo.segments[0] !== streamEntry.name) {
|
|
13669
|
+
continue;
|
|
13670
|
+
}
|
|
13671
|
+
const reachable = collectReachablePaths(stateElement, registry, scan.name);
|
|
13672
|
+
for (const dep of streamEntry.depAddresses) {
|
|
13673
|
+
const depPath = dep.absolutePathInfo.pathInfo.path;
|
|
13674
|
+
if (reachable.has(depPath)) {
|
|
13675
|
+
raiseError(`[wcs/scan-feedback-loop] ${STATE_STREAMS_NAME} entry "${streamEntry.name}" args read "${depPath}", ` +
|
|
13676
|
+
`which is derived from the ${STATE_SCAN_NAME} output "${scan.name}" that this stream feeds. ` +
|
|
13677
|
+
`Every landing would restart the stream on its own result. Advance the cursor from an event ($on) ` +
|
|
13678
|
+
`and keep it a plain property instead of deriving it from the accumulator.`);
|
|
13679
|
+
}
|
|
13680
|
+
}
|
|
13681
|
+
}
|
|
13682
|
+
}
|
|
13683
|
+
|
|
13033
13684
|
/**
|
|
13034
13685
|
* stream/argsTrace.ts
|
|
13035
13686
|
*
|
|
@@ -13348,6 +13999,8 @@ function startStream(stateElement, entry) {
|
|
|
13348
13999
|
// args 評価 + 依存の per-run 再捕捉(args === null なら depAddresses を clear して
|
|
13349
14000
|
// undefined。Promise / 自己依存 / wildcard 読みは raiseError、§3-1)
|
|
13350
14001
|
const argsValue = traceArgs(stateElement, entry);
|
|
14002
|
+
// scan 出力 → args の前進ループを起動前に止める(docs/state-scan-design.md D9)
|
|
14003
|
+
assertNoScanFeedback(stateElement, entry);
|
|
13351
14004
|
// 値リセット: setByAddress を通すことで updater coalesce・sameValueGuard・
|
|
13352
14005
|
// walkDependency(stream 値に依存する computed の dirty 化)がすべて乗る(§3-3)
|
|
13353
14006
|
stateElement.createState("writable", (state) => {
|
|
@@ -13509,6 +14162,7 @@ registerUpdateBatchListener(restartStreamsOnUpdateBatch, STREAM_LISTENER_PRIORIT
|
|
|
13509
14162
|
* どちらの経路も必ずここを通るため「Set に居る = 接続中かつ宣言済み」が保たれる。
|
|
13510
14163
|
* この「宣言済み」の側が崩れると、`$watch` 未使用アプリの drain にも収集ループが乗る
|
|
13511
14164
|
* (ゼロコスト契約、docs/state-watch-hook-design.md §10)。
|
|
14165
|
+
* `$scan` の drain / enqueue のゲート(scan/scanRegistry.ts)も同じ出入りで数える。
|
|
13512
14166
|
*/
|
|
13513
14167
|
const registryByStateElement = new WeakMap();
|
|
13514
14168
|
/**
|
|
@@ -13546,6 +14200,7 @@ function getWatchEntries(stateElement) {
|
|
|
13546
14200
|
*/
|
|
13547
14201
|
function addActiveWatchStateElement(stateElement) {
|
|
13548
14202
|
activeStateElements.add(stateElement);
|
|
14203
|
+
activateScanGates(stateElement);
|
|
13549
14204
|
}
|
|
13550
14205
|
/**
|
|
13551
14206
|
* 発火対象を列挙する(drain リスナーの early return 判定用)。
|
|
@@ -13562,12 +14217,14 @@ function getActiveWatchStateElements() {
|
|
|
13562
14217
|
*/
|
|
13563
14218
|
function deactivateWatch(stateElement) {
|
|
13564
14219
|
activeStateElements.delete(stateElement);
|
|
14220
|
+
deactivateScanGates(stateElement);
|
|
13565
14221
|
}
|
|
13566
14222
|
/**
|
|
13567
14223
|
* registry から削除し、発火対象からも外す(`_state` 再 set 時の再配線用)。
|
|
13568
14224
|
*/
|
|
13569
14225
|
function clearWatchRegistry(stateElement) {
|
|
13570
14226
|
activeStateElements.delete(stateElement);
|
|
14227
|
+
deactivateScanGates(stateElement);
|
|
13571
14228
|
registryByStateElement.delete(stateElement);
|
|
13572
14229
|
}
|
|
13573
14230
|
/** ボリュームの watch entry を追記する(置換しない)。 */
|
|
@@ -13762,53 +14419,625 @@ function clearPrevValues() {
|
|
|
13762
14419
|
}
|
|
13763
14420
|
|
|
13764
14421
|
/**
|
|
13765
|
-
* watch/
|
|
13766
|
-
*
|
|
13767
|
-
* `$watch` の発火(docs/state-watch-hook-design.md §3 / §7)。
|
|
13768
|
-
*
|
|
13769
|
-
* updater の drain 終了フックに 1 つだけリスナーを登録し、バッチに載った絶対アドレスと
|
|
13770
|
-
* 宣言済み watch パスを突き合わせて発火する。**binding の有無に関係なくバッチへ載る**ので、
|
|
13771
|
-
* これが headless 購読の実体になる(binding 駆動の `$updatedCallback` との違い)。
|
|
14422
|
+
* watch/rowLanding.ts
|
|
13772
14423
|
*
|
|
13773
|
-
*
|
|
13774
|
-
*
|
|
13775
|
-
* `$updatedCallback` が先なのは binding 適用ループの内側で呼ばれる構造的必然。
|
|
13776
|
-
* - watch ハンドラ間は `$watch` の宣言順(entry.order)。利用者が順序に意思を持てる唯一の層。
|
|
13777
|
-
* - 同一パスの複数行は indexes 昇順。
|
|
14424
|
+
* ワイルドカードのパスの行の着地を、drain の時点のリストの位置 1 つにつき 1 つに絞る(#274)。
|
|
14425
|
+
* `$watch` と `$scan` の `from` が共有する(docs/state-scan-design.md D3・§5-5)。
|
|
13778
14426
|
*
|
|
13779
|
-
*
|
|
13780
|
-
*
|
|
13781
|
-
*
|
|
13782
|
-
*
|
|
14427
|
+
* バッチに載る行のアドレスは、書き込みの時点の行を指す。同じ job でその後にリストを短くした・
|
|
14428
|
+
* 置き換えた・途中から取り除いた・`list.*` へ要素を書き込んだと、drain の時点ではアドレスの行が
|
|
14429
|
+
* もうその位置に居ないことがある。添字で読むと、範囲外の読みで throw するか、その位置にいま居る
|
|
14430
|
+
* 別の行の値で発火する。
|
|
13783
14431
|
*/
|
|
13784
14432
|
/**
|
|
13785
|
-
*
|
|
13786
|
-
* `
|
|
13787
|
-
*
|
|
13788
|
-
* `addActiveWatchStateElement` の薄いラッパではなく、**State が runtime を import する
|
|
13789
|
-
* 経路をここに一本化する**意味がある: drain リスナーの登録はこのモジュールの
|
|
13790
|
-
* 初期化副作用なので、registry だけを import すると発火機構ごと落ちる。
|
|
13791
|
-
* `$streams` の `startStreams` と対称の位置づけ。
|
|
13792
|
-
*
|
|
13793
|
-
* **宣言が 1 つも無ければ active 集合に入れない**(`startStreams` の
|
|
13794
|
-
* `entries.size === 0` early return と同型)。ここを無条件にすると active 集合が
|
|
13795
|
-
* 「接続中の全 `<wcs-state>`」になり、`fireWatchOnUpdateBatch` の early return が
|
|
13796
|
-
* 実アプリで効かなくなる = `$watch` 未使用アプリの drain にも収集ループが乗る
|
|
13797
|
-
* (ゼロコスト契約、設計書 §10 / 実装計画 P16)。
|
|
14433
|
+
* 行の連鎖に、差分がリストから外した(退役した)行があるか。読むだけの前判定で、リストは読まない。
|
|
14434
|
+
* `$watch` はこれが真のヒット(か、同じ位置に重なるヒット)があるときだけ位置を引く。
|
|
13798
14435
|
*/
|
|
13799
|
-
function
|
|
13800
|
-
|
|
13801
|
-
|
|
14436
|
+
function hasRetiredRow(listIndex) {
|
|
14437
|
+
for (let row = listIndex; row !== null; row = row.parentListIndex) {
|
|
14438
|
+
if (isRetiredListIndex(row)) {
|
|
14439
|
+
return true;
|
|
14440
|
+
}
|
|
13802
14441
|
}
|
|
13803
|
-
|
|
13804
|
-
primeComputedWatches(stateElement);
|
|
14442
|
+
return false;
|
|
13805
14443
|
}
|
|
13806
14444
|
/**
|
|
13807
|
-
*
|
|
14445
|
+
* 行のアドレスが、いまのリストのその位置とどう関係するか。外側の段から台帳を引く。
|
|
14446
|
+
* 読むだけ(`$scan` の計画の相・D18)なので、台帳は観測用の `getListIndexesByList` で引き、
|
|
14447
|
+
* 親ポインタを修理する `resolveListIndexesByList` は使わない。
|
|
14448
|
+
* - `gone`: その位置に行が無い(リストを短くした・空にした・配列でなくした)か、アドレスの行を
|
|
14449
|
+
* 差分がリストから外した(退役した — 行を取り除いた・リストを置き換えた)。
|
|
14450
|
+
* - `current`: その位置の行が、アドレスの行そのものか、同じリスト要素を表す行。
|
|
14451
|
+
* - `replaced`: その位置には別の要素の行が居るが、アドレスの行は差分に外されていない
|
|
14452
|
+
* (`list.*` への要素の書き込みは、差分を通らずに台帳のその位置へ別の行を差し込む)。
|
|
14453
|
+
*/
|
|
14454
|
+
function placementOf(state, pathInfo, row) {
|
|
14455
|
+
// ワイルドカードのパスの着地は、収集の段階で listIndex を持つものに限っている
|
|
14456
|
+
const listIndex = row.absAddress.listIndex;
|
|
14457
|
+
let placement = "current";
|
|
14458
|
+
let parentListIndex = null;
|
|
14459
|
+
for (let level = 0; level < pathInfo.wildcardCount; level++) {
|
|
14460
|
+
const list = state[getByAddressSymbol](createStateAddress(pathInfo.wildcardParentPathInfos[level], parentListIndex));
|
|
14461
|
+
// 台帳を持たない値(配列でない・空の配列)は行が無い
|
|
14462
|
+
const current = getListIndexesByList(list)?.[row.indexes[level]];
|
|
14463
|
+
if (typeof current === "undefined") {
|
|
14464
|
+
return "gone";
|
|
14465
|
+
}
|
|
14466
|
+
// `$scan` も `$watch` もルートのツリーでだけ発火するので、行の連鎖にスコープの base 段は無い(段は必ずある)
|
|
14467
|
+
const own = listIndexAtWildcard(listIndex, level, pathInfo.wildcardCount);
|
|
14468
|
+
if (current !== own && !isSameListIndexValue(current, own)) {
|
|
14469
|
+
if (isRetiredListIndex(own)) {
|
|
14470
|
+
return "gone";
|
|
14471
|
+
}
|
|
14472
|
+
placement = "replaced";
|
|
14473
|
+
}
|
|
14474
|
+
parentListIndex = current;
|
|
14475
|
+
}
|
|
14476
|
+
return placement;
|
|
14477
|
+
}
|
|
14478
|
+
/**
|
|
14479
|
+
* 行の着地を、いまのリストの位置 1 つにつき 1 つに絞る。戻り値の順は、位置ごとに最初に残した行の順。
|
|
13808
14480
|
*
|
|
13809
|
-
*
|
|
13810
|
-
*
|
|
13811
|
-
*
|
|
14481
|
+
* - 位置に行が無いアドレス(行を書いてからリストを短くした・空にした)は捨てる。
|
|
14482
|
+
* 添字で読むと範囲外の読みで throw する。
|
|
14483
|
+
* - 差分がリストから外した行のアドレス(行を書いてからその行を取り除いた・リストを置き換えた)は捨てる。
|
|
14484
|
+
* 添字で読むと、その位置にいま居る行の値を読む — 置き換えで入った行は自分のアドレスで着地するので二重になり、
|
|
14485
|
+
* 位置だけが移ってきた行は変わっていないのに着地する(#274・docs/state-scan-design.md §5-5)。
|
|
14486
|
+
* - 同じ位置に、いまそこに居る行のアドレスと、差分に外されていない別の行のアドレス(`list.*` への要素の
|
|
14487
|
+
* 書き込みの前にその行へ書いた形)が並んだら前者を残す。後者しか無い位置は残し、その位置のいまの値を読ませる。
|
|
14488
|
+
*/
|
|
14489
|
+
function selectLandedRows(state, pathInfo, rows) {
|
|
14490
|
+
const byPosition = new Map();
|
|
14491
|
+
for (const row of rows) {
|
|
14492
|
+
const placement = placementOf(state, pathInfo, row);
|
|
14493
|
+
if (placement === "gone") {
|
|
14494
|
+
continue;
|
|
14495
|
+
}
|
|
14496
|
+
const rank = placement === "current" ? 1 : 0;
|
|
14497
|
+
const key = row.indexes.join(",");
|
|
14498
|
+
const kept = byPosition.get(key);
|
|
14499
|
+
if (typeof kept === "undefined" || rank > kept.rank) {
|
|
14500
|
+
byPosition.set(key, { row, rank });
|
|
14501
|
+
}
|
|
14502
|
+
}
|
|
14503
|
+
return Array.from(byPosition.values(), (kept) => kept.row);
|
|
14504
|
+
}
|
|
14505
|
+
|
|
14506
|
+
/**
|
|
14507
|
+
* scan/initialValue.ts
|
|
14508
|
+
*
|
|
14509
|
+
* 出力に置く `initial` の複製と、「出力がいま `initial` と同じ値か」の判定
|
|
14510
|
+
* (docs/state-scan-design.md D6 / D7・§5-9)。
|
|
14511
|
+
*
|
|
14512
|
+
* 実体化と reset が宣言の `initial` をそのまま置くと、出力が `initial` の間に子パスへ書いた値
|
|
14513
|
+
* (`$watch` ハンドラの注記など)が宣言の `initial` 自体を書き換える。以後の reset はその値に戻し、
|
|
14514
|
+
* 出力が同じ参照なので書き込みもしない。そこで plain なデータは複製して置く。
|
|
14515
|
+
*
|
|
14516
|
+
* plain なデータ: プロトタイプが `Array.prototype` / `Object.prototype` / null で、凍結されておらず、
|
|
14517
|
+
* 自前のプロパティがすべて列挙できる文字列キーのデータプロパティ(配列は添字と `length` だけ)の
|
|
14518
|
+
* 配列とオブジェクト。判定は property descriptor で行い、getter を実行しない。それ以外
|
|
14519
|
+
* (getter / setter・Symbol キー・列挙できないプロパティ・配列の追加プロパティ・Array のサブクラス・
|
|
14520
|
+
* 凍結された値・関数・クラスのインスタンス・Map / Set・Date・DOM ノード)は参照のまま置く。
|
|
14521
|
+
*
|
|
14522
|
+
* 「reset で出力が既に `initial` なら書かない」は、参照ではなく値で見る。plain なデータは中身を再帰で
|
|
14523
|
+
* 比べ、それ以外は同一性で比べる。訪れた対を覚えるので、形の違う循環でも止まる。
|
|
14524
|
+
*/
|
|
14525
|
+
const ARRAY_INDEX = /^(?:0|[1-9]\d*)$/;
|
|
14526
|
+
function isEnumerableData(target, key) {
|
|
14527
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, key);
|
|
14528
|
+
return descriptor.enumerable === true && "value" in descriptor;
|
|
14529
|
+
}
|
|
14530
|
+
/** plain なデータならその種類、そうでなければ null(参照のまま扱う)。 */
|
|
14531
|
+
function plainKind(value) {
|
|
14532
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value)) {
|
|
14533
|
+
return null;
|
|
14534
|
+
}
|
|
14535
|
+
const proto = Object.getPrototypeOf(value);
|
|
14536
|
+
if (Array.isArray(value)) {
|
|
14537
|
+
const plainArray = proto === Array.prototype && Reflect.ownKeys(value).every((key) => key === "length" || (typeof key === "string" && ARRAY_INDEX.test(key) && isEnumerableData(value, key)));
|
|
14538
|
+
return plainArray ? "array" : null;
|
|
14539
|
+
}
|
|
14540
|
+
const plainObject = (proto === Object.prototype || proto === null) && Reflect.ownKeys(value).every((key) => typeof key === "string" && isEnumerableData(value, key));
|
|
14541
|
+
return plainObject ? "object" : null;
|
|
14542
|
+
}
|
|
14543
|
+
function cloneValue(value, copies) {
|
|
14544
|
+
const kind = plainKind(value);
|
|
14545
|
+
if (kind === null) {
|
|
14546
|
+
return value;
|
|
14547
|
+
}
|
|
14548
|
+
const source = value;
|
|
14549
|
+
const known = copies.get(source);
|
|
14550
|
+
if (typeof known !== "undefined") {
|
|
14551
|
+
return known;
|
|
14552
|
+
}
|
|
14553
|
+
let copy;
|
|
14554
|
+
if (kind === "array") {
|
|
14555
|
+
// 穴はそのまま(添字のキーだけを写す)
|
|
14556
|
+
copy = new Array(value.length);
|
|
14557
|
+
}
|
|
14558
|
+
else {
|
|
14559
|
+
copy = Object.getPrototypeOf(value) === null ? Object.create(null) : {};
|
|
14560
|
+
}
|
|
14561
|
+
copies.set(source, copy);
|
|
14562
|
+
for (const key of Object.keys(source)) {
|
|
14563
|
+
copy[key] = cloneValue(source[key], copies);
|
|
14564
|
+
}
|
|
14565
|
+
return copy;
|
|
14566
|
+
}
|
|
14567
|
+
/** plain なデータを再帰で複製する。それ以外の値は参照のまま返す(循環と共有参照も保つ)。 */
|
|
14568
|
+
function cloneInitial(initial) {
|
|
14569
|
+
return cloneValue(initial, new Map());
|
|
14570
|
+
}
|
|
14571
|
+
function sameValue(current, initial, compared) {
|
|
14572
|
+
if (Object.is(current, initial)) {
|
|
14573
|
+
return true;
|
|
14574
|
+
}
|
|
14575
|
+
// 宣言側の種類を先に決め、配列の長さ・キーの数が違えば、出力側の descriptor を見ずに抜ける
|
|
14576
|
+
// (大きく積み上がった出力を空の initial と比べるたびに、出力の大きさに比例させない)
|
|
14577
|
+
const kind = plainKind(initial);
|
|
14578
|
+
if (kind === null || typeof current !== "object" || current === null) {
|
|
14579
|
+
return false;
|
|
14580
|
+
}
|
|
14581
|
+
const left = current;
|
|
14582
|
+
const right = initial;
|
|
14583
|
+
if (kind === "array") {
|
|
14584
|
+
if (!Array.isArray(current) || current.length !== initial.length) {
|
|
14585
|
+
return false;
|
|
14586
|
+
}
|
|
14587
|
+
}
|
|
14588
|
+
else if (Array.isArray(current) || Object.keys(left).length !== Object.keys(right).length) {
|
|
14589
|
+
return false;
|
|
14590
|
+
}
|
|
14591
|
+
if (plainKind(current) !== kind) {
|
|
14592
|
+
return false;
|
|
14593
|
+
}
|
|
14594
|
+
// 訪れた対を出力側の値ごとの集合で覚える。形の違う循環(自己循環と 2 段の循環など)でも、同じ対の 2 回目で止まる
|
|
14595
|
+
let seen = compared.get(left);
|
|
14596
|
+
if (typeof seen === "undefined") {
|
|
14597
|
+
seen = new Set();
|
|
14598
|
+
compared.set(left, seen);
|
|
14599
|
+
}
|
|
14600
|
+
else if (seen.has(right)) {
|
|
14601
|
+
return true;
|
|
14602
|
+
}
|
|
14603
|
+
seen.add(right);
|
|
14604
|
+
if (kind === "array") {
|
|
14605
|
+
for (let index = 0; index < current.length; index++) {
|
|
14606
|
+
if (!sameValue(left[index], right[index], compared)) {
|
|
14607
|
+
return false;
|
|
14608
|
+
}
|
|
14609
|
+
}
|
|
14610
|
+
return true;
|
|
14611
|
+
}
|
|
14612
|
+
return Object.keys(right).every((key) => Object.prototype.hasOwnProperty.call(left, key) && sameValue(left[key], right[key], compared));
|
|
14613
|
+
}
|
|
14614
|
+
/** 出力が `initial` と同じ値か(plain なデータは中身を、それ以外は同一性を比べる)。 */
|
|
14615
|
+
function isSameAsInitial(current, initial) {
|
|
14616
|
+
return sameValue(current, initial, new Map());
|
|
14617
|
+
}
|
|
14618
|
+
/**
|
|
14619
|
+
* 実体化・fold・reset が出力に置いた関数値と、その出力名(docs/state-scan-design.md D7・§5-9 の C5-14)。
|
|
14620
|
+
*
|
|
14621
|
+
* fold が関数を返す出力は、再セットの宣言検査で state の関数値として見える。それがこの出力に置いた値なら
|
|
14622
|
+
* 累積で、メソッドとの衝突ではない。どの世代の state オブジェクトに書かれたか(世代を進めた後に throw した
|
|
14623
|
+
* 再セットの後は、新しい `__state` にだけ書かれる)に依らず判定できるよう、値そのものを記録する。
|
|
14624
|
+
* 関数は WeakMap のキーなので、出力から外れれば記録ごと回収される。
|
|
14625
|
+
*/
|
|
14626
|
+
const outputNamesByFunction = new WeakMap();
|
|
14627
|
+
/** 出力に置いた値を記録する(関数のときだけ残す)。 */
|
|
14628
|
+
function recordOutputValue(name, value) {
|
|
14629
|
+
if (typeof value !== "function") {
|
|
14630
|
+
return;
|
|
14631
|
+
}
|
|
14632
|
+
const names = outputNamesByFunction.get(value);
|
|
14633
|
+
if (typeof names === "undefined") {
|
|
14634
|
+
outputNamesByFunction.set(value, new Set([name]));
|
|
14635
|
+
}
|
|
14636
|
+
else {
|
|
14637
|
+
names.add(name);
|
|
14638
|
+
}
|
|
14639
|
+
}
|
|
14640
|
+
/** その関数値を、実体化・fold・reset がこの出力に置いたことがあるか。 */
|
|
14641
|
+
function wasPlacedOnOutput(name, value) {
|
|
14642
|
+
return typeof value === "function" && outputNamesByFunction.get(value)?.has(name) === true;
|
|
14643
|
+
}
|
|
14644
|
+
|
|
14645
|
+
/**
|
|
14646
|
+
* scan/scanReport.ts
|
|
14647
|
+
*
|
|
14648
|
+
* `$scan` の失敗の報告(docs/state-scan-design.md D4 / D5)。
|
|
14649
|
+
*
|
|
14650
|
+
* 値の読み・fold・出力の書き込みの例外は scan 側で閉じる(drain リスナーと event-token の
|
|
14651
|
+
* emit ループを他の機構と共有しているため)。閉じた事実を console と devtools の両方に出す。
|
|
14652
|
+
* devtools には `$watch` と同じ `state:watch-error` を path `$scan.<出力名>` で流し、phase は
|
|
14653
|
+
* 直し方で分ける: 値を読めなかった(`evaluate`)・fold が契約を破った(`fold`)・出力を
|
|
14654
|
+
* 書けなかった(`write`)。
|
|
14655
|
+
*/
|
|
14656
|
+
const SCAN_FAILURE_MESSAGE = {
|
|
14657
|
+
"read-source": (name) => `${STATE_SCAN_NAME} could not read a "from" value for "${name}". That landing was not folded; the other landings of the batch still were.`,
|
|
14658
|
+
"read-output": (name) => `${STATE_SCAN_NAME} could not read the output "${name}". Nothing was folded or written.`,
|
|
14659
|
+
threw: (name) => `${STATE_SCAN_NAME} fold for "${name}" threw. The output was not written.`,
|
|
14660
|
+
"returned-promise": (name) => `${STATE_SCAN_NAME} fold for "${name}" returned a Promise. fold must be synchronous and return the next value; the output was not written.`,
|
|
14661
|
+
write: (name) => `${STATE_SCAN_NAME} could not write the output "${name}".`,
|
|
14662
|
+
};
|
|
14663
|
+
const SCAN_FAILURE_PHASE = {
|
|
14664
|
+
"read-source": "evaluate",
|
|
14665
|
+
"read-output": "evaluate",
|
|
14666
|
+
threw: "fold",
|
|
14667
|
+
"returned-promise": "fold",
|
|
14668
|
+
write: "write",
|
|
14669
|
+
};
|
|
14670
|
+
function sendToDevtools(name, phase, error) {
|
|
14671
|
+
if (devtoolsSink !== null) {
|
|
14672
|
+
devtoolsSink({
|
|
14673
|
+
type: "state:watch-error",
|
|
14674
|
+
phase,
|
|
14675
|
+
path: `${STATE_SCAN_NAME}.${name}`,
|
|
14676
|
+
error,
|
|
14677
|
+
});
|
|
14678
|
+
}
|
|
14679
|
+
}
|
|
14680
|
+
function reportScanError(name, error, failure) {
|
|
14681
|
+
console.error(`[@wcstack/state] ${SCAN_FAILURE_MESSAGE[failure](name)}`, error);
|
|
14682
|
+
sendToDevtools(name, SCAN_FAILURE_PHASE[failure], error);
|
|
14683
|
+
}
|
|
14684
|
+
function isThenable(value) {
|
|
14685
|
+
return value !== null
|
|
14686
|
+
&& (typeof value === "object" || typeof value === "function")
|
|
14687
|
+
&& typeof value.then === "function";
|
|
14688
|
+
}
|
|
14689
|
+
/**
|
|
14690
|
+
* fold が thenable を返した(同期契約違反)。書き込まずに報告し、
|
|
14691
|
+
* 後から reject されても unhandled rejection にしない。
|
|
14692
|
+
*/
|
|
14693
|
+
function reportScanThenable(name, value) {
|
|
14694
|
+
value.then(undefined, () => undefined);
|
|
14695
|
+
reportScanError(name, new TypeError(`${STATE_SCAN_NAME} fold for "${name}" returned a Promise. fold must be synchronous and return the next value.`), "returned-promise");
|
|
14696
|
+
}
|
|
14697
|
+
const lateGetterReported = new WeakSet();
|
|
14698
|
+
/**
|
|
14699
|
+
* 宣言時には plain だった `from` が、接ぎ木(ボリュームのアクセサ登録)で後から getter に
|
|
14700
|
+
* なった(設計書 D5 後段)。getter の再評価回数を畳まないよう、その scan を止める。
|
|
14701
|
+
* entry につき 1 回だけ報告する。fold を適用しないと決めた失敗なので devtools の phase は `fold`。
|
|
14702
|
+
*/
|
|
14703
|
+
function reportLateGetterSource(entry, name, path) {
|
|
14704
|
+
if (lateGetterReported.has(entry)) {
|
|
14705
|
+
return;
|
|
14706
|
+
}
|
|
14707
|
+
lateGetterReported.add(entry);
|
|
14708
|
+
const message = `[wcs/scan-source-computed] ${STATE_SCAN_NAME} entry "${name}" from "${path}" is now backed by a getter ` +
|
|
14709
|
+
`(registered after the declaration, e.g. by a volume). A getter re-evaluates whenever its inputs change, ` +
|
|
14710
|
+
`so this scan stopped folding. Fold the plain value the getter reads instead.`;
|
|
14711
|
+
console.error(`[@wcstack/state] ${message}`);
|
|
14712
|
+
sendToDevtools(name, "fold", new Error(message));
|
|
14713
|
+
}
|
|
14714
|
+
|
|
14715
|
+
/**
|
|
14716
|
+
* scan/scanRuntime.ts
|
|
14717
|
+
*
|
|
14718
|
+
* `$scan` の drain 側の発火 — `from` と `resetOn`(docs/state-scan-design.md §2-1)。
|
|
14719
|
+
*
|
|
14720
|
+
* watch runtime の drain リスナー(`WATCH_LISTENER_PRIORITY`)から呼ばれる(D11)。
|
|
14721
|
+
* 発火対象集合・連鎖深さ・`prev` 台帳は `$watch` と共有する。
|
|
14722
|
+
*
|
|
14723
|
+
* 発火単位(D3): バッチに載った `from` の絶対アドレス 1 つにつき fold 1 回。
|
|
14724
|
+
* 同じ出力へ複数行(wildcard)が載ったバッチでは、indexes 昇順に acc を連鎖させて
|
|
14725
|
+
* 最後に 1 回だけ書く。開始値と `Object.is` で同じなら書かない。
|
|
14726
|
+
* 行の着地は、いまのリストの位置 1 つにつき 1 回に絞る(watch/rowLanding.ts の selectLandedRows — `$watch` と共有)。
|
|
14727
|
+
*
|
|
14728
|
+
* 計画と書き込みを 2 相に分ける(D18): 全 scan の次の値を読むだけで決め(planScansOnUpdateBatch)、
|
|
14729
|
+
* 宣言順に書く(commitScanPlans)。どちらも同じ drain の `$watch` の発火より前(D11)。
|
|
14730
|
+
* - 1 相で「畳んでは書く」と、ある scan の出力を `from` に取る後続の scan が、同じ drain で
|
|
14731
|
+
* 先行 scan が書いたばかりの(まだバッチとして届いていない)値を畳み、次のバッチで同じ値を
|
|
14732
|
+
* もう一度畳む — 届いていた値は取りこぼす。宣言順しだいで exactly-once が破れる。
|
|
14733
|
+
* - 書き込みが `$watch` より前なので、同じ drain の `$watch` ハンドラは畳んで書いた後の出力を読み、ハンドラが
|
|
14734
|
+
* 出力へ書いた値はそのまま残る。その代わり、出力の source を drain のリスナーが書き進めて、出力の着地と
|
|
14735
|
+
* source の新しい着地が同じバッチに載る連鎖では、出力を見る `$watch` の prev が前の着地を持ち、cur が
|
|
14736
|
+
* 1 段先行し、同じ値で 2 回発火し得る(D12 の契約・§5-6 の差し戻し)。
|
|
14737
|
+
*
|
|
14738
|
+
* 失敗は種類ごとに閉じる(D4): 読めない行はその行だけを捨てて連鎖を続け(`$watch` が行ごとに
|
|
14739
|
+
* evaluate で閉じるのと同じ)、出力の読み・fold の throw はその scan の書き込みを止める。
|
|
14740
|
+
*/
|
|
14741
|
+
const NO_PLANS = Object.freeze([]);
|
|
14742
|
+
function groupOf(groups, stateElement, entry) {
|
|
14743
|
+
let group = groups.get(entry);
|
|
14744
|
+
if (typeof group === "undefined") {
|
|
14745
|
+
group = { stateElement, entry, rows: [], reset: false };
|
|
14746
|
+
groups.set(entry, group);
|
|
14747
|
+
}
|
|
14748
|
+
return group;
|
|
14749
|
+
}
|
|
14750
|
+
/**
|
|
14751
|
+
* 同じ entry の行は wildcard の段数が同じ(indexes の長さが等しい)なので、
|
|
14752
|
+
* 外側の段から最初に違う段で比べる。
|
|
14753
|
+
*/
|
|
14754
|
+
function compareRows(a, b) {
|
|
14755
|
+
let level = 0;
|
|
14756
|
+
while (level < a.indexes.length - 1 && a.indexes[level] === b.indexes[level]) {
|
|
14757
|
+
level++;
|
|
14758
|
+
}
|
|
14759
|
+
return a.indexes[level] - b.indexes[level];
|
|
14760
|
+
}
|
|
14761
|
+
/**
|
|
14762
|
+
* `from` の根が `$streams` 名で、そのバッチにその stream の restart 依存が載っているか(D10)。
|
|
14763
|
+
* 載っていれば同じ drain の STREAM リスナーが run を abort する。到着した chunk は
|
|
14764
|
+
* 捨てられる run のものなので畳まない(`$streams` §3-2「restart が勝つ」)。
|
|
14765
|
+
*/
|
|
14766
|
+
function isStreamRestartPending(stateElement, rootName, batch) {
|
|
14767
|
+
const streamEntry = getStreamEntries(stateElement).get(rootName);
|
|
14768
|
+
if (typeof streamEntry === "undefined") {
|
|
14769
|
+
return false;
|
|
14770
|
+
}
|
|
14771
|
+
for (const dep of streamEntry.depAddresses) {
|
|
14772
|
+
if (batch.has(dep)) {
|
|
14773
|
+
return true;
|
|
14774
|
+
}
|
|
14775
|
+
}
|
|
14776
|
+
return false;
|
|
14777
|
+
}
|
|
14778
|
+
function hasGetterOnPath(stateElement, source) {
|
|
14779
|
+
for (const path of source.pathInfo.cumulativePaths) {
|
|
14780
|
+
if (stateElement.getterPaths.has(path)) {
|
|
14781
|
+
return true;
|
|
14782
|
+
}
|
|
14783
|
+
}
|
|
14784
|
+
return false;
|
|
14785
|
+
}
|
|
14786
|
+
/** 先行する fold や書き込みが同期に切断・再セットを起こし得る(`$watch` と同じ再確認) */
|
|
14787
|
+
function isLive(group, activeStateElements) {
|
|
14788
|
+
return activeStateElements.has(group.stateElement)
|
|
14789
|
+
&& getScanRegistry(group.stateElement)?.entries.has(group.entry) === true;
|
|
14790
|
+
}
|
|
14791
|
+
/**
|
|
14792
|
+
* 発火しないと決まった group の、`on` scan の保留 reset を捨てる(scan/eventReset.ts)。
|
|
14793
|
+
* `on` の group は `resetOn` のヒットからしか作られない。同じ `resetOn` のパスが次のバッチ向けに
|
|
14794
|
+
* 積まれていれば、その保留は次のバッチの drain が使うので残す(discardSkippedScanResets と同じ規則)。
|
|
14795
|
+
*/
|
|
14796
|
+
function dropPendingReset(group) {
|
|
14797
|
+
const { entry, stateElement } = group;
|
|
14798
|
+
if (entry.source.kind === "event" && !entry.resetOn.some((path) => isQueuedForNextBatch(stateElement, path))) {
|
|
14799
|
+
consumePendingScanReset(entry);
|
|
14800
|
+
}
|
|
14801
|
+
}
|
|
14802
|
+
/** 次のバッチ(まだ drain されていない書き込み)に、この state のこのパスが積まれているか */
|
|
14803
|
+
function isQueuedForNextBatch(stateElement, path) {
|
|
14804
|
+
return getUpdater().hasQueuedPath(stateElement, path);
|
|
14805
|
+
}
|
|
14806
|
+
/**
|
|
14807
|
+
* このバッチの scan を発火しない drain(発火対象でない state・連鎖深さの上限)で、`on` scan の
|
|
14808
|
+
* 保留 reset を捨てる。`firing` はこの drain で scan を発火する state の集合、null はバッチ全体を
|
|
14809
|
+
* 発火しない。同じ `resetOn` のパスが次のバッチ向けに積まれていれば、その保留は残す。
|
|
14810
|
+
*/
|
|
14811
|
+
function discardSkippedScanResets(batch, firing) {
|
|
14812
|
+
discardPendingScanResets(batch, firing, isQueuedForNextBatch);
|
|
14813
|
+
}
|
|
14814
|
+
function readSource(state, source, row) {
|
|
14815
|
+
return source.pathInfo.wildcardCount === 0
|
|
14816
|
+
? state[source.path]
|
|
14817
|
+
: state.$resolve(source.path, row.indexes);
|
|
14818
|
+
}
|
|
14819
|
+
/** 相 1: 次の値を読むだけで決める。書き込みは無いか null。 */
|
|
14820
|
+
function planGroup(group, batch, activeStateElements) {
|
|
14821
|
+
if (!isLive(group, activeStateElements)) {
|
|
14822
|
+
dropPendingReset(group);
|
|
14823
|
+
return null;
|
|
14824
|
+
}
|
|
14825
|
+
const { stateElement, entry } = group;
|
|
14826
|
+
if (group.reset) {
|
|
14827
|
+
// reset が勝つ(D6)。同じバッチの行は畳まない
|
|
14828
|
+
return { group, next: entry.initial };
|
|
14829
|
+
}
|
|
14830
|
+
// reset でない group は `from` の行ヒットから作られている
|
|
14831
|
+
const source = entry.source;
|
|
14832
|
+
if (hasGetterOnPath(stateElement, source)) {
|
|
14833
|
+
reportLateGetterSource(entry, entry.name, source.path);
|
|
14834
|
+
return null;
|
|
14835
|
+
}
|
|
14836
|
+
if (isStreamRestartPending(stateElement, source.pathInfo.segments[0], batch)) {
|
|
14837
|
+
return null;
|
|
14838
|
+
}
|
|
14839
|
+
// コールバック内の代入は制御フロー解析に載らないので、結果と進み具合は入れ物で受け取る
|
|
14840
|
+
const result = { plan: null, failure: "read-output" };
|
|
14841
|
+
try {
|
|
14842
|
+
stateElement.createState("readonly", (state) => {
|
|
14843
|
+
const start = state[entry.name];
|
|
14844
|
+
const rows = source.pathInfo.wildcardCount === 0
|
|
14845
|
+
? group.rows
|
|
14846
|
+
: selectLandedRows(state, source.pathInfo, group.rows);
|
|
14847
|
+
result.failure = "threw";
|
|
14848
|
+
const fold = entry.fold;
|
|
14849
|
+
let acc = start;
|
|
14850
|
+
for (const row of rows.sort(compareRows)) {
|
|
14851
|
+
let cur;
|
|
14852
|
+
try {
|
|
14853
|
+
cur = readSource(state, source, row);
|
|
14854
|
+
}
|
|
14855
|
+
catch (error) {
|
|
14856
|
+
// 読めない行だけを捨てて、残りの行の連鎖は続ける
|
|
14857
|
+
reportScanError(entry.name, error, "read-source");
|
|
14858
|
+
continue;
|
|
14859
|
+
}
|
|
14860
|
+
const next = fold(acc, cur, getPrevValue(row.absAddress), ...row.indexes);
|
|
14861
|
+
if (isThenable(next)) {
|
|
14862
|
+
reportScanThenable(entry.name, next);
|
|
14863
|
+
return;
|
|
14864
|
+
}
|
|
14865
|
+
acc = next;
|
|
14866
|
+
}
|
|
14867
|
+
if (!Object.is(acc, start)) {
|
|
14868
|
+
result.plan = { group, next: acc };
|
|
14869
|
+
}
|
|
14870
|
+
});
|
|
14871
|
+
}
|
|
14872
|
+
catch (error) {
|
|
14873
|
+
// 他の scan・`$watch`・`$streams` restart を巻き添えにしない(D4)
|
|
14874
|
+
reportScanError(entry.name, error, result.failure);
|
|
14875
|
+
}
|
|
14876
|
+
return result.plan;
|
|
14877
|
+
}
|
|
14878
|
+
/** 相 2 の 1 件: 書く直前に再確認する(後続の scan の fold が同期に切断・再セットし得る)。 */
|
|
14879
|
+
function commitPlan(plan, activeStateElements) {
|
|
14880
|
+
const { group } = plan;
|
|
14881
|
+
if (!isLive(group, activeStateElements)) {
|
|
14882
|
+
dropPendingReset(group);
|
|
14883
|
+
return;
|
|
14884
|
+
}
|
|
14885
|
+
const { stateElement, entry } = group;
|
|
14886
|
+
// `on` の reset は保留が残っているときだけ。書き込みの後に来た出来事が `initial` から
|
|
14887
|
+
// 畳んで保留を消していれば、その出力は reset 済み(scan/eventReset.ts)
|
|
14888
|
+
if (group.reset && entry.source.kind === "event" && !consumePendingScanReset(entry)) {
|
|
14889
|
+
return;
|
|
14890
|
+
}
|
|
14891
|
+
try {
|
|
14892
|
+
stateElement.createState("writable", (state) => {
|
|
14893
|
+
const current = state[entry.name];
|
|
14894
|
+
// reset は既に initial と同じ値なら書かない(着地も `$watch` の発火も起こさない)。書くときは複製を置く(D6 / D7)
|
|
14895
|
+
const writes = group.reset ? !isSameAsInitial(current, entry.initial) : !Object.is(current, plan.next);
|
|
14896
|
+
if (writes) {
|
|
14897
|
+
const next = group.reset ? cloneInitial(entry.initial) : plan.next;
|
|
14898
|
+
state[entry.name] = next;
|
|
14899
|
+
recordOutputValue(entry.name, next);
|
|
14900
|
+
}
|
|
14901
|
+
});
|
|
14902
|
+
}
|
|
14903
|
+
catch (error) {
|
|
14904
|
+
reportScanError(entry.name, error, "write");
|
|
14905
|
+
}
|
|
14906
|
+
}
|
|
14907
|
+
/**
|
|
14908
|
+
* 相 1: バッチの `from` / `resetOn` ヒットを集め、宣言順に次の値を決める(書かない)。
|
|
14909
|
+
* `depth` は watch runtime が消費した連鎖深さ。fold の中の書き込みも連鎖に数える。
|
|
14910
|
+
*/
|
|
14911
|
+
function planScansOnUpdateBatch(batch, activeStateElements, depth) {
|
|
14912
|
+
// 発火対象でない state(書き込みの後に切断された)の `on` scan の保留 reset を捨てる(D6)。
|
|
14913
|
+
// drain のゲートより前に置く — 切断で最後の scan がゲートから外れても、その書き込みの保留は残っている
|
|
14914
|
+
discardSkippedScanResets(batch, activeStateElements);
|
|
14915
|
+
if (getScanDrainGateCount() === 0) {
|
|
14916
|
+
return NO_PLANS;
|
|
14917
|
+
}
|
|
14918
|
+
const groups = new Map();
|
|
14919
|
+
for (const absAddress of batch) {
|
|
14920
|
+
const stateElement = absAddress.absolutePathInfo.stateElement;
|
|
14921
|
+
if (!activeStateElements.has(stateElement)) {
|
|
14922
|
+
continue;
|
|
14923
|
+
}
|
|
14924
|
+
const registry = getScanRegistry(stateElement);
|
|
14925
|
+
if (typeof registry === "undefined") {
|
|
14926
|
+
continue;
|
|
14927
|
+
}
|
|
14928
|
+
const pathInfo = absAddress.absolutePathInfo.pathInfo;
|
|
14929
|
+
const fromEntries = registry.byFromPath.get(pathInfo.path);
|
|
14930
|
+
if (typeof fromEntries !== "undefined") {
|
|
14931
|
+
let indexes = [];
|
|
14932
|
+
if (pathInfo.wildcardCount > 0) {
|
|
14933
|
+
if (absAddress.listIndex === null) {
|
|
14934
|
+
// 行が特定できないヒット(リストの依存展開で載る中間アドレス等)。`$watch` と同じく落とす
|
|
14935
|
+
continue;
|
|
14936
|
+
}
|
|
14937
|
+
indexes = getScopedIndexes(absAddress.listIndex, pathInfo.wildcardCount);
|
|
14938
|
+
}
|
|
14939
|
+
for (const entry of fromEntries) {
|
|
14940
|
+
groupOf(groups, stateElement, entry).rows.push({ absAddress, indexes });
|
|
14941
|
+
}
|
|
14942
|
+
}
|
|
14943
|
+
const resetEntries = registry.byResetPath.get(pathInfo.path);
|
|
14944
|
+
if (typeof resetEntries !== "undefined") {
|
|
14945
|
+
for (const entry of resetEntries) {
|
|
14946
|
+
groupOf(groups, stateElement, entry).reset = true;
|
|
14947
|
+
}
|
|
14948
|
+
}
|
|
14949
|
+
}
|
|
14950
|
+
if (groups.size === 0) {
|
|
14951
|
+
return NO_PLANS;
|
|
14952
|
+
}
|
|
14953
|
+
const ordered = Array.from(groups.values()).sort((a, b) => a.entry.order - b.entry.order);
|
|
14954
|
+
const plans = [];
|
|
14955
|
+
beginWatchFiring(depth);
|
|
14956
|
+
try {
|
|
14957
|
+
for (const group of ordered) {
|
|
14958
|
+
const plan = planGroup(group, batch, activeStateElements);
|
|
14959
|
+
if (plan !== null) {
|
|
14960
|
+
plans.push(plan);
|
|
14961
|
+
}
|
|
14962
|
+
}
|
|
14963
|
+
}
|
|
14964
|
+
finally {
|
|
14965
|
+
endWatchFiring();
|
|
14966
|
+
}
|
|
14967
|
+
return plans;
|
|
14968
|
+
}
|
|
14969
|
+
/**
|
|
14970
|
+
* 相 2: 計画を宣言順に書く(D18)。同じ drain の `$watch` の発火より前(D11)。
|
|
14971
|
+
* scan の書き込みも連鎖深さに数える。
|
|
14972
|
+
*/
|
|
14973
|
+
function commitScanPlans(plans, activeStateElements, depth) {
|
|
14974
|
+
if (plans.length === 0) {
|
|
14975
|
+
return;
|
|
14976
|
+
}
|
|
14977
|
+
beginWatchFiring(depth);
|
|
14978
|
+
try {
|
|
14979
|
+
for (const plan of plans) {
|
|
14980
|
+
commitPlan(plan, activeStateElements);
|
|
14981
|
+
}
|
|
14982
|
+
}
|
|
14983
|
+
finally {
|
|
14984
|
+
endWatchFiring();
|
|
14985
|
+
}
|
|
14986
|
+
}
|
|
14987
|
+
|
|
14988
|
+
/**
|
|
14989
|
+
* watch/watchRuntime.ts
|
|
14990
|
+
*
|
|
14991
|
+
* `$watch` の発火(docs/state-watch-hook-design.md §3 / §7)。
|
|
14992
|
+
*
|
|
14993
|
+
* updater の drain 終了フックに 1 つだけリスナーを登録し、バッチに載った絶対アドレスと
|
|
14994
|
+
* 宣言済み watch パスを突き合わせて発火する。**binding の有無に関係なくバッチへ載る**ので、
|
|
14995
|
+
* これが headless 購読の実体になる(binding 駆動の `$updatedCallback` との違い)。
|
|
14996
|
+
*
|
|
14997
|
+
* 実行順序(設計書 §3-2):
|
|
14998
|
+
* - 機構間は優先度で固定(`$updatedCallback` → `$scan` → `$watch` → `$streams` restart)。
|
|
14999
|
+
* `$updatedCallback` が先なのは binding 適用ループの内側で呼ばれる構造的必然。
|
|
15000
|
+
* `$scan` の `from` / `resetOn` は同じリスナーの中で `$watch` より先に畳んで書く
|
|
15001
|
+
* (docs/state-scan-design.md D11 / D18)。
|
|
15002
|
+
* - watch ハンドラ間は `$watch` の宣言順(entry.order)。利用者が順序に意思を持てる唯一の層。
|
|
15003
|
+
* - 同一パスの複数行は indexes 昇順。
|
|
15004
|
+
*
|
|
15005
|
+
* 収集と発火を 2 相に分ける理由:
|
|
15006
|
+
* 1. ハンドラ内の書き込みが registry / active 集合を同期的に変えうる(`_state` 再 set・切断)
|
|
15007
|
+
* ため、発火直前に live 再チェックが要る(`$streams` の restart hits と同型)。
|
|
15008
|
+
* 2. 上記の順序規約のために hits をソートする必要がある(バッチの反復順は enqueue 順)。
|
|
15009
|
+
*/
|
|
15010
|
+
/**
|
|
15011
|
+
* この stateElement の `$watch` を有効化する(`State.connectedCallback` /接続中の
|
|
15012
|
+
* `_state` 再 set から呼ばれる。無効化は `clearWatchRegistry`)。
|
|
15013
|
+
*
|
|
15014
|
+
* `addActiveWatchStateElement` の薄いラッパではなく、**State が runtime を import する
|
|
15015
|
+
* 経路をここに一本化する**意味がある: drain リスナーの登録はこのモジュールの
|
|
15016
|
+
* 初期化副作用なので、registry だけを import すると発火機構ごと落ちる。
|
|
15017
|
+
* `$streams` の `startStreams` と対称の位置づけ。
|
|
15018
|
+
*
|
|
15019
|
+
* **宣言が 1 つも無ければ active 集合に入れない**(`startStreams` の
|
|
15020
|
+
* `entries.size === 0` early return と同型)。ここを無条件にすると active 集合が
|
|
15021
|
+
* 「接続中の全 `<wcs-state>`」になり、`fireWatchOnUpdateBatch` の early return が
|
|
15022
|
+
* 実アプリで効かなくなる = `$watch` 未使用アプリの drain にも収集ループが乗る
|
|
15023
|
+
* (ゼロコスト契約、設計書 §10 / 実装計画 P16)。
|
|
15024
|
+
*
|
|
15025
|
+
* `$scan` の `from` / `resetOn` も同じ drain リスナーで発火するので、それだけを宣言した
|
|
15026
|
+
* state も発火対象に載せる(docs/state-scan-design.md D11)。
|
|
15027
|
+
*/
|
|
15028
|
+
function startWatch(stateElement) {
|
|
15029
|
+
if (getWatchEntries(stateElement).size === 0 && getVolumeWatchEntries(stateElement).size === 0 && !hasScanDrainWork(stateElement)) {
|
|
15030
|
+
return;
|
|
15031
|
+
}
|
|
15032
|
+
addActiveWatchStateElement(stateElement);
|
|
15033
|
+
primeComputedWatches(stateElement);
|
|
15034
|
+
}
|
|
15035
|
+
/**
|
|
15036
|
+
* watch 対象の computed(getter)を 1 回評価する(設計書 §5-2 の eager 化、C-3)。
|
|
15037
|
+
*
|
|
15038
|
+
* これが要るのは、getter の依存(dynamicDependency)が**評価時にしか張られない**ため。
|
|
15039
|
+
* 一度も評価されていない getter は依存グラフに載らず、依存の書き込みが walkDependency で
|
|
15040
|
+
* そのパスへ到達しないので、バッチにも載らず watch が永久に発火しない。ここで 1 回
|
|
13812
15041
|
* 読むことで依存が張られ、同時に `prev` の初期スナップショットが埋まる。
|
|
13813
15042
|
*
|
|
13814
15043
|
* **これが「watch した getter は lazy でなくなる」の実体**であり、設計書 §5-2 で
|
|
@@ -13819,7 +15048,7 @@ function startWatch(stateElement) {
|
|
|
13819
15048
|
* 「DOM にバインドされていれば発火する」ままとし、§5-3 に制約として書く。
|
|
13820
15049
|
*/
|
|
13821
15050
|
function primeComputedWatches(stateElement) {
|
|
13822
|
-
//
|
|
15051
|
+
// 宣言(watch か scan)が 1 つ以上あることは startWatch が保証済み。scan だけなら targets は空
|
|
13823
15052
|
const targets = [];
|
|
13824
15053
|
for (const entry of getWatchEntries(stateElement).values()) {
|
|
13825
15054
|
if (isScalarComputed(stateElement, entry)) {
|
|
@@ -13898,84 +15127,158 @@ function fireWatchOnUpdateBatch(batch) {
|
|
|
13898
15127
|
// ここも finally を通す: 宣言済みの state が切断されている間(active からは
|
|
13899
15128
|
// 外れるが watchPaths は残る)の書き込みで台帳に旧値が積まれるため、
|
|
13900
15129
|
// クリアを早期 return の外に置くと次のバッチどころか永久に残る。
|
|
15130
|
+
// 書き込みの後・drain の前に切断された state の `on` scan の保留 reset も、このバッチでは
|
|
15131
|
+
// 発火しないので捨てる(scan/eventReset.ts)。
|
|
15132
|
+
discardSkippedScanResets(batch, activeStateElements);
|
|
13901
15133
|
return;
|
|
13902
15134
|
}
|
|
13903
15135
|
const depth = consumeWatchChainDepth();
|
|
13904
15136
|
if (depth > MAX_WATCH_CHAIN_DEPTH) {
|
|
13905
|
-
//
|
|
13906
|
-
//
|
|
15137
|
+
// 打ち切るのは同じリスナーの発火(`$scan` の from / resetOn と `$watch`)のみ。
|
|
15138
|
+
// 値と binding 適用は巻き戻さない(伝播 hop 上限超過時の quarantine と同じ姿勢、§7-2)。
|
|
15139
|
+
// `on` scan の保留 reset もこのバッチでは効かせない(`from` の scan が reset しないのと揃える)。
|
|
15140
|
+
discardSkippedScanResets(batch, null);
|
|
13907
15141
|
const paths = Array.from(batch, (absAddress) => absAddress.absolutePathInfo.pathInfo.path);
|
|
13908
|
-
console.error(`[@wcstack/state] $watch chain depth limit exceeded; watch handlers for this batch were skipped.`, { maxDepth: MAX_WATCH_CHAIN_DEPTH, paths });
|
|
15142
|
+
console.error(`[@wcstack/state] $watch chain depth limit exceeded; $scan folds and $watch handlers for this batch were skipped.`, { maxDepth: MAX_WATCH_CHAIN_DEPTH, paths });
|
|
13909
15143
|
if (devtoolsSink !== null) {
|
|
13910
15144
|
devtoolsSink({ type: "state:watch-chain-limit", maxDepth: MAX_WATCH_CHAIN_DEPTH, paths });
|
|
13911
15145
|
}
|
|
13912
15146
|
return;
|
|
13913
15147
|
}
|
|
13914
|
-
//
|
|
13915
|
-
|
|
13916
|
-
|
|
13917
|
-
|
|
13918
|
-
|
|
13919
|
-
|
|
13920
|
-
|
|
13921
|
-
|
|
13922
|
-
|
|
13923
|
-
|
|
15148
|
+
// `$scan` の from / resetOn を `$watch` より先に畳んで書く(docs/state-scan-design.md D11 / D18)。
|
|
15149
|
+
// scan の書き込みは次のバッチに乗るので、この drain の `$watch` の収集には影響しない。同じ drain の
|
|
15150
|
+
// `$watch` ハンドラは書いた後の出力を読み、ハンドラが出力へ書いた値はそのまま残る。
|
|
15151
|
+
commitScanPlans(planScansOnUpdateBatch(batch, activeStateElements, depth), activeStateElements, depth);
|
|
15152
|
+
fireWatchHits(batch, activeStateElements, depth);
|
|
15153
|
+
}
|
|
15154
|
+
finally {
|
|
15155
|
+
// 旧値台帳はこの drain 限りのもの。次のバッチへ持ち越さない(§4-1)。
|
|
15156
|
+
clearPrevValues();
|
|
15157
|
+
}
|
|
15158
|
+
}
|
|
15159
|
+
/** バッチの `$watch` ヒットを集めて(収集フェーズ)、宣言順・indexes 昇順に発火する(発火フェーズ)。 */
|
|
15160
|
+
function fireWatchHits(batch, activeStateElements, depth) {
|
|
15161
|
+
// --- 収集フェーズ ---
|
|
15162
|
+
const hits = [];
|
|
15163
|
+
for (const absAddress of batch) {
|
|
15164
|
+
// stateElement 参照で引く。AbsolutePathInfo は
|
|
15165
|
+
// stateElement 単位でキャッシュされるので、同名 state が複数の rootNode に
|
|
15166
|
+
// 居ても取り違えない(address/AbsolutePathInfo.ts)。他 state のアドレスは
|
|
15167
|
+
// ここで自然に落ちる = 越境しない(設計 D8)。
|
|
15168
|
+
const stateElement = absAddress.absolutePathInfo.stateElement;
|
|
15169
|
+
if (!activeStateElements.has(stateElement)) {
|
|
15170
|
+
continue;
|
|
15171
|
+
}
|
|
15172
|
+
const path = absAddress.absolutePathInfo.pathInfo.path;
|
|
15173
|
+
const own = getWatchEntries(stateElement).get(path);
|
|
15174
|
+
const fromVolumes = getVolumeWatchEntries(stateElement).get(path);
|
|
15175
|
+
if (typeof own === "undefined" && typeof fromVolumes === "undefined") {
|
|
15176
|
+
continue;
|
|
15177
|
+
}
|
|
15178
|
+
const matched = typeof own === "undefined" ? [] : [own];
|
|
15179
|
+
if (typeof fromVolumes !== "undefined") {
|
|
15180
|
+
matched.push(...fromVolumes);
|
|
15181
|
+
}
|
|
15182
|
+
for (const entry of matched) {
|
|
15183
|
+
let indexes = [];
|
|
15184
|
+
if (entry.pathInfo.wildcardCount > 0) {
|
|
15185
|
+
if (absAddress.listIndex === null) {
|
|
15186
|
+
// ワイルドカードパスなのに行が特定できないヒット(リストの依存展開で載る
|
|
15187
|
+
// 中間アドレス等)。indexes を空のまま発火すると cur の解決($resolve)が
|
|
15188
|
+
// 「indexes 不足」で throw し、例外隔離に落ちて console.error だけが残る。
|
|
15189
|
+
// 行が定まらない以上ハンドラに渡せる意味が無いので、収集段階で落とす。
|
|
15190
|
+
continue;
|
|
15191
|
+
}
|
|
15192
|
+
indexes = getScopedIndexes(absAddress.listIndex, entry.pathInfo.wildcardCount);
|
|
13924
15193
|
}
|
|
13925
|
-
|
|
13926
|
-
|
|
13927
|
-
|
|
13928
|
-
|
|
15194
|
+
hits.push({ stateElement, entry, absAddress, indexes });
|
|
15195
|
+
}
|
|
15196
|
+
}
|
|
15197
|
+
if (hits.length === 0) {
|
|
15198
|
+
return;
|
|
15199
|
+
}
|
|
15200
|
+
hits.sort(compareHits);
|
|
15201
|
+
const landed = selectWatchLandings(hits);
|
|
15202
|
+
// --- 発火フェーズ ---
|
|
15203
|
+
beginWatchFiring(depth);
|
|
15204
|
+
try {
|
|
15205
|
+
for (const hit of landed) {
|
|
15206
|
+
// 先行ハンドラが同期的に切断や `_state` 再 set を行い得るため、発火直前に
|
|
15207
|
+
// 「まだ active か」「entry が現行 registry のものか」を再確認する。
|
|
15208
|
+
if (!activeStateElements.has(hit.stateElement)) {
|
|
13929
15209
|
continue;
|
|
13930
15210
|
}
|
|
13931
|
-
const
|
|
13932
|
-
|
|
13933
|
-
|
|
13934
|
-
|
|
13935
|
-
for (const entry of matched) {
|
|
13936
|
-
let indexes = [];
|
|
13937
|
-
if (entry.pathInfo.wildcardCount > 0) {
|
|
13938
|
-
if (absAddress.listIndex === null) {
|
|
13939
|
-
// ワイルドカードパスなのに行が特定できないヒット(リストの依存展開で載る
|
|
13940
|
-
// 中間アドレス等)。indexes を空のまま発火すると cur の解決($resolve)が
|
|
13941
|
-
// 「indexes 不足」で throw し、例外隔離に落ちて console.error だけが残る。
|
|
13942
|
-
// 行が定まらない以上ハンドラに渡せる意味が無いので、収集段階で落とす。
|
|
13943
|
-
continue;
|
|
13944
|
-
}
|
|
13945
|
-
indexes = getScopedIndexes(absAddress.listIndex, entry.pathInfo.wildcardCount);
|
|
13946
|
-
}
|
|
13947
|
-
hits.push({ stateElement, entry, absAddress, indexes });
|
|
15211
|
+
const stillOwn = getWatchEntries(hit.stateElement).get(hit.entry.path) === hit.entry;
|
|
15212
|
+
const stillVolume = getVolumeWatchEntries(hit.stateElement).get(hit.entry.path)?.includes(hit.entry) === true;
|
|
15213
|
+
if (!stillOwn && !stillVolume) {
|
|
15214
|
+
continue;
|
|
13948
15215
|
}
|
|
15216
|
+
fireOne(hit);
|
|
13949
15217
|
}
|
|
13950
|
-
|
|
13951
|
-
|
|
15218
|
+
}
|
|
15219
|
+
finally {
|
|
15220
|
+
endWatchFiring();
|
|
15221
|
+
}
|
|
15222
|
+
}
|
|
15223
|
+
/**
|
|
15224
|
+
* ワイルドカードの watch の行の着地を、いまのリストの位置 1 つにつき 1 つに絞る(`$scan` の `from` と
|
|
15225
|
+
* 同じ選別 — watch/rowLanding.ts・#274)。`hits` は compareHits でソート済みで、戻り値もその順を保つ。
|
|
15226
|
+
*
|
|
15227
|
+
* 位置を引くにはリストを読むので、引くのは退役した行を含むヒットがあるか、同じ entry・同じ indexes の
|
|
15228
|
+
* ヒットが並ぶ(ソート済みなので隣り合う)entry だけ。行の値を書くだけのバッチ(大多数)は
|
|
15229
|
+
* WeakSet の引きだけで抜ける。
|
|
15230
|
+
*/
|
|
15231
|
+
function selectWatchLandings(hits) {
|
|
15232
|
+
let entries = null;
|
|
15233
|
+
for (let i = 0; i < hits.length; i++) {
|
|
15234
|
+
const hit = hits[i];
|
|
15235
|
+
if (hit.entry.pathInfo.wildcardCount === 0) {
|
|
15236
|
+
continue;
|
|
13952
15237
|
}
|
|
13953
|
-
hits
|
|
13954
|
-
//
|
|
13955
|
-
|
|
15238
|
+
const previous = hits[i - 1];
|
|
15239
|
+
// ワイルドカードの hit は収集の段階で listIndex を持つものに限っている
|
|
15240
|
+
if (hasRetiredRow(hit.absAddress.listIndex)
|
|
15241
|
+
|| (previous?.entry === hit.entry && isSameIndexes(previous.indexes, hit.indexes))) {
|
|
15242
|
+
(entries ??= new Set()).add(hit.entry);
|
|
15243
|
+
}
|
|
15244
|
+
}
|
|
15245
|
+
if (entries === null) {
|
|
15246
|
+
return hits;
|
|
15247
|
+
}
|
|
15248
|
+
const dropped = new Set();
|
|
15249
|
+
for (const entry of entries) {
|
|
15250
|
+
// entry は 1 つの state 要素の registry に属するので、group の state 要素は 1 つ
|
|
15251
|
+
const group = hits.filter((hit) => hit.entry === entry);
|
|
15252
|
+
const stateElement = group[0].stateElement;
|
|
13956
15253
|
try {
|
|
13957
|
-
|
|
13958
|
-
|
|
13959
|
-
|
|
13960
|
-
|
|
13961
|
-
|
|
13962
|
-
|
|
13963
|
-
const stillOwn = getWatchEntries(hit.stateElement).get(hit.entry.path) === hit.entry;
|
|
13964
|
-
const stillVolume = getVolumeWatchEntries(hit.stateElement).get(hit.entry.path)?.includes(hit.entry) === true;
|
|
13965
|
-
if (!stillOwn && !stillVolume) {
|
|
13966
|
-
continue;
|
|
15254
|
+
stateElement.createState("readonly", (state) => {
|
|
15255
|
+
const kept = new Set(selectLandedRows(state, entry.pathInfo, group));
|
|
15256
|
+
for (const hit of group) {
|
|
15257
|
+
if (!kept.has(hit)) {
|
|
15258
|
+
dropped.add(hit);
|
|
15259
|
+
}
|
|
13967
15260
|
}
|
|
13968
|
-
|
|
13969
|
-
}
|
|
15261
|
+
});
|
|
13970
15262
|
}
|
|
13971
|
-
|
|
13972
|
-
|
|
15263
|
+
catch (e) {
|
|
15264
|
+
// 位置を引く読み(リストのパスの getter など)が throw した。同じリストを通る cur も読めないので、
|
|
15265
|
+
// この entry のヒットは発火せず、評価の失敗として 1 回報告する(他の watch は続行する)
|
|
15266
|
+
reportWatchError(stateElement, entry.path, "evaluate", e);
|
|
15267
|
+
for (const hit of group) {
|
|
15268
|
+
dropped.add(hit);
|
|
15269
|
+
}
|
|
13973
15270
|
}
|
|
13974
15271
|
}
|
|
13975
|
-
|
|
13976
|
-
|
|
13977
|
-
|
|
15272
|
+
return hits.filter((hit) => !dropped.has(hit));
|
|
15273
|
+
}
|
|
15274
|
+
/** 同じ entry の hit どうし(indexes の長さは等しい) */
|
|
15275
|
+
function isSameIndexes(a, b) {
|
|
15276
|
+
for (let i = 0; i < a.length; i++) {
|
|
15277
|
+
if (a[i] !== b[i]) {
|
|
15278
|
+
return false;
|
|
15279
|
+
}
|
|
13978
15280
|
}
|
|
15281
|
+
return true;
|
|
13979
15282
|
}
|
|
13980
15283
|
/**
|
|
13981
15284
|
* 層 2(宣言順)→ 層 3(indexes 昇順)の順に比較する(設計書 §3-3)。
|
|
@@ -14050,6 +15353,407 @@ function readCurrentValue(state, entry, indexes) {
|
|
|
14050
15353
|
// 優先度で `$streams` の restart より先に固定する(設計書 §3-2 層 1)。import 順には依存しない。
|
|
14051
15354
|
registerUpdateBatchListener(fireWatchOnUpdateBatch, WATCH_LISTENER_PRIORITY);
|
|
14052
15355
|
|
|
15356
|
+
/**
|
|
15357
|
+
* scan/processScanDeclaration.ts
|
|
15358
|
+
*
|
|
15359
|
+
* `$scan: { <output>: { from | on, initial, fold, resetOn? } }` の解析と配線
|
|
15360
|
+
* (docs/state-scan-design.md §1-2 / §2-4)。
|
|
15361
|
+
*
|
|
15362
|
+
* `_state` セッター上の位置が違う 3 段に分ける:
|
|
15363
|
+
* 1. `parseScanDeclaration` — `value` だけを読む検査。世代を進める**前**に走るので、
|
|
15364
|
+
* ここで throw した再セットは要素を丸ごと旧世代に残す。
|
|
15365
|
+
* 2. `materializeScanOutputs` / `subscribeScanEvents` — `clearEventTokenRegistry` の直後。
|
|
15366
|
+
* 実体化を `_rebuildPathInfo` より前に置くのは、前世代の `from`(別 scan の出力の子)を
|
|
15367
|
+
* 張り直すときに存在検査が偽の miss を出さないため。`on` の購読を `$on` より前に置くのは、
|
|
15368
|
+
* 同じトークンで reducer → effect の順にするため(D11)。
|
|
15369
|
+
* 3. `registerScans` — `processWatchDeclaration` の直前(`_pathSet` クリア後)。
|
|
15370
|
+
*/
|
|
15371
|
+
const INVALID = "[wcs/scan-declaration-invalid]";
|
|
15372
|
+
const COMPUTED = "[wcs/scan-source-computed]";
|
|
15373
|
+
const NO_RESET = Object.freeze([]);
|
|
15374
|
+
function entryLabel(name) {
|
|
15375
|
+
return `${STATE_SCAN_NAME} entry "${name}"`;
|
|
15376
|
+
}
|
|
15377
|
+
/** 累積パス(`a` / `a.b` / `a.b.c`)のうち最初に getter であるものを返す */
|
|
15378
|
+
function findGetterOnPath(pathInfo, getterPaths) {
|
|
15379
|
+
for (const path of pathInfo.cumulativePaths) {
|
|
15380
|
+
if (getterPaths.has(path)) {
|
|
15381
|
+
return path;
|
|
15382
|
+
}
|
|
15383
|
+
}
|
|
15384
|
+
return null;
|
|
15385
|
+
}
|
|
15386
|
+
function computedMessage(label, field, path, getter) {
|
|
15387
|
+
const where = getter === path
|
|
15388
|
+
? "is a getter"
|
|
15389
|
+
: getter.includes(`${WILDCARD}${WILDCARD}`)
|
|
15390
|
+
? `is computed by the recursive getter "${getter}"`
|
|
15391
|
+
: `is under the getter "${getter}"`;
|
|
15392
|
+
return `${COMPUTED} ${label} ${field} "${path}" ${where}. A getter re-evaluates whenever its inputs change, ` +
|
|
15393
|
+
`so folding it would count re-evaluations, not events. Fold the plain value the getter reads, or use "on" with an event token.${LINT_HINT}`;
|
|
15394
|
+
}
|
|
15395
|
+
function writeOnlyMessage(label, path, setter) {
|
|
15396
|
+
const where = setter === path ? "is a setter without a getter" : `is under the setter without a getter "${setter}"`;
|
|
15397
|
+
return `${INVALID} ${label} from "${path}" ${where}, so it always reads undefined and every fold would receive undefined. ` +
|
|
15398
|
+
`Fold the plain value the setter writes, or use "on" with an event token.${LINT_HINT}`;
|
|
15399
|
+
}
|
|
15400
|
+
function assertValidScanPath(label, field, path) {
|
|
15401
|
+
if (path.length === 0) {
|
|
15402
|
+
raiseError(`${INVALID} ${label} "${field}" must be a non-empty state path.${LINT_HINT}`);
|
|
15403
|
+
}
|
|
15404
|
+
if (path.startsWith("$")) {
|
|
15405
|
+
raiseError(`${INVALID} ${label} ${field} "${path}" must not start with "$" (reserved namespace).${LINT_HINT}`);
|
|
15406
|
+
}
|
|
15407
|
+
if (path.includes("@")) {
|
|
15408
|
+
raiseError(`${INVALID} ${label} ${field} "${path}" must not contain "@" — there is a single state tree per root.${LINT_HINT}`);
|
|
15409
|
+
}
|
|
15410
|
+
if (path in Object.prototype) {
|
|
15411
|
+
raiseError(`${INVALID} ${label} ${field} "${path}" must not be a property name inherited from Object.prototype (e.g. "__proto__", "constructor").${LINT_HINT}`);
|
|
15412
|
+
}
|
|
15413
|
+
const pathInfo = getPathInfo(path);
|
|
15414
|
+
for (const segment of pathInfo.segments) {
|
|
15415
|
+
if (segment.length === 0) {
|
|
15416
|
+
raiseError(`${INVALID} ${label} ${field} "${path}" has an empty path segment.${LINT_HINT}`);
|
|
15417
|
+
}
|
|
15418
|
+
}
|
|
15419
|
+
if (pathInfo.wildcardCount > MAX_WILDCARD_DEPTH) {
|
|
15420
|
+
raiseError(`${INVALID} ${label} ${field} "${path}" exceeds the maximum wildcard depth (${MAX_WILDCARD_DEPTH}).`);
|
|
15421
|
+
}
|
|
15422
|
+
return pathInfo;
|
|
15423
|
+
}
|
|
15424
|
+
function collectStreamNames(state) {
|
|
15425
|
+
const streams = state[STATE_STREAMS_NAME];
|
|
15426
|
+
if (typeof streams !== "object" || streams === null) {
|
|
15427
|
+
return new Set();
|
|
15428
|
+
}
|
|
15429
|
+
return new Set(Object.keys(streams));
|
|
15430
|
+
}
|
|
15431
|
+
function assertOutputName(name, getterPaths, setterPaths, streamNames) {
|
|
15432
|
+
const label = entryLabel(name);
|
|
15433
|
+
if (name.length === 0) {
|
|
15434
|
+
raiseError(`${INVALID} ${STATE_SCAN_NAME} entry name must be a non-empty string.${LINT_HINT}`);
|
|
15435
|
+
}
|
|
15436
|
+
if (name.includes(DELIMITER) || name.includes(WILDCARD)) {
|
|
15437
|
+
raiseError(`${INVALID} ${label} must be a flat property name ("${DELIMITER}" and "${WILDCARD}" are not allowed). The output is a property the runtime owns.${LINT_HINT}`);
|
|
15438
|
+
}
|
|
15439
|
+
if (name.startsWith("$")) {
|
|
15440
|
+
raiseError(`${INVALID} ${label} must not start with "$" (reserved namespace).${LINT_HINT}`);
|
|
15441
|
+
}
|
|
15442
|
+
if (name in Object.prototype) {
|
|
15443
|
+
raiseError(`${INVALID} ${label} must not be a property name inherited from Object.prototype (e.g. "__proto__", "constructor").${LINT_HINT}`);
|
|
15444
|
+
}
|
|
15445
|
+
if (getterPaths.has(name)) {
|
|
15446
|
+
raiseError(`${INVALID} ${label} conflicts with a getter declared on the state.${LINT_HINT}`);
|
|
15447
|
+
}
|
|
15448
|
+
if (setterPaths.has(name)) {
|
|
15449
|
+
raiseError(`${INVALID} ${label} conflicts with a setter declared on the state.${LINT_HINT}`);
|
|
15450
|
+
}
|
|
15451
|
+
if (streamNames.has(name)) {
|
|
15452
|
+
raiseError(`${INVALID} ${label} conflicts with the ${STATE_STREAMS_NAME} entry of the same name — each output has exactly one owner.${LINT_HINT}`);
|
|
15453
|
+
}
|
|
15454
|
+
}
|
|
15455
|
+
/**
|
|
15456
|
+
* 出力名がメソッド(関数値のプロパティ)と衝突していないか(D7)。
|
|
15457
|
+
*
|
|
15458
|
+
* 実体化は既にある値を保持するので、衝突を通すと最初の `acc` がメソッドになり、fold の結果が
|
|
15459
|
+
* メソッドを上書きする。`initial` 自身が関数で、その値が置かれている形だけは通す —
|
|
15460
|
+
* 同じオブジェクトの再セットで、実体化済みの出力を見ているだけだから。
|
|
15461
|
+
*/
|
|
15462
|
+
function assertNoMethodConflict(name, functionValues, definition) {
|
|
15463
|
+
const value = functionValues.get(name);
|
|
15464
|
+
// fold・実体化・reset がこの出力に置いたことのある関数値なら、それは累積(同じオブジェクト・値のコピーの
|
|
15465
|
+
// 再セット・D7)。どの世代のオブジェクトに書かれたかに依らない。名前だけで通すと、新しいオブジェクトの
|
|
15466
|
+
// 同名の本物のメソッドを見逃す(§5-9 の C5-8 / C5-14)
|
|
15467
|
+
if (functionValues.has(name) && value !== definition.initial && !wasPlacedOnOutput(name, value)) {
|
|
15468
|
+
raiseError(`${INVALID} ${entryLabel(name)} conflicts with a method (a function-valued property) declared on the state. The output is a property the runtime owns.${LINT_HINT}`);
|
|
15469
|
+
}
|
|
15470
|
+
}
|
|
15471
|
+
function parseSource(name, definition, eventTokenNames, getterPaths, writeOnlyPaths, recursion) {
|
|
15472
|
+
const label = entryLabel(name);
|
|
15473
|
+
const hasFrom = typeof definition.from !== "undefined";
|
|
15474
|
+
const hasOn = typeof definition.on !== "undefined";
|
|
15475
|
+
if (hasFrom === hasOn) {
|
|
15476
|
+
raiseError(`${INVALID} ${label} must declare exactly one of "from" (a state path) or "on" (an event-token name).${LINT_HINT}`);
|
|
15477
|
+
}
|
|
15478
|
+
if (hasOn) {
|
|
15479
|
+
const tokenName = definition.on;
|
|
15480
|
+
if (typeof tokenName !== "string" || tokenName.length === 0) {
|
|
15481
|
+
raiseError(`${INVALID} ${label} "on" must be a non-empty event-token name.${LINT_HINT}`);
|
|
15482
|
+
}
|
|
15483
|
+
if (!eventTokenNames.has(tokenName)) {
|
|
15484
|
+
raiseError(`${INVALID} ${label} on "${tokenName}" is not declared in ${STATE_EVENT_TOKENS_NAME}.${didYouMean(tokenName, eventTokenNames)}${LINT_HINT}`);
|
|
15485
|
+
}
|
|
15486
|
+
return { kind: "event", tokenName };
|
|
15487
|
+
}
|
|
15488
|
+
const path = definition.from;
|
|
15489
|
+
if (typeof path !== "string") {
|
|
15490
|
+
raiseError(`${INVALID} ${label} "from" must be a state path string.${LINT_HINT}`);
|
|
15491
|
+
}
|
|
15492
|
+
const pathInfo = assertValidScanPath(label, "from", path);
|
|
15493
|
+
// `**` getter の展開形(`nodes.*.total`)とその値の内側も getter。字面の getterPaths には `**` の形しか
|
|
15494
|
+
// 載らないので、再帰レジストリで引く(pathDiagnostics の checkDeclaredPath と同じ判定)。
|
|
15495
|
+
// 展開形は必ず `*` を含むので `resetOn` はワイルドカードの検査で先に落ちる
|
|
15496
|
+
const getter = findGetterOnPath(pathInfo, getterPaths) ?? recursion?.recursiveGetterOwning(path) ?? null;
|
|
15497
|
+
if (getter !== null) {
|
|
15498
|
+
raiseError(computedMessage(label, "from", path, getter));
|
|
15499
|
+
}
|
|
15500
|
+
// getter の無い setter は読むと常に undefined(`resetOn` は値を読まない引き金なので通す)
|
|
15501
|
+
const setter = findGetterOnPath(pathInfo, writeOnlyPaths);
|
|
15502
|
+
if (setter !== null) {
|
|
15503
|
+
raiseError(writeOnlyMessage(label, path, setter));
|
|
15504
|
+
}
|
|
15505
|
+
if (path === name || path.startsWith(name + DELIMITER)) {
|
|
15506
|
+
raiseError(`${INVALID} ${label} from "${path}" reads the entry's own output — the scan would fold its own writes forever.${LINT_HINT}`);
|
|
15507
|
+
}
|
|
15508
|
+
return { kind: "path", path, pathInfo };
|
|
15509
|
+
}
|
|
15510
|
+
function parseResetOn(name, raw, source, getterPaths) {
|
|
15511
|
+
if (typeof raw === "undefined") {
|
|
15512
|
+
return NO_RESET;
|
|
15513
|
+
}
|
|
15514
|
+
const label = entryLabel(name);
|
|
15515
|
+
if (!Array.isArray(raw)) {
|
|
15516
|
+
raiseError(`${INVALID} ${label} "resetOn" must be an array of state paths.${LINT_HINT}`);
|
|
15517
|
+
}
|
|
15518
|
+
const paths = [];
|
|
15519
|
+
for (const item of raw) {
|
|
15520
|
+
if (typeof item !== "string") {
|
|
15521
|
+
raiseError(`${INVALID} ${label} "resetOn" must contain only state path strings.${LINT_HINT}`);
|
|
15522
|
+
}
|
|
15523
|
+
const pathInfo = assertValidScanPath(label, "resetOn", item);
|
|
15524
|
+
if (pathInfo.wildcardCount > 0) {
|
|
15525
|
+
raiseError(`${INVALID} ${label} resetOn "${item}" must not contain "${WILDCARD}" — a reset returns the whole output to "initial".${LINT_HINT}`);
|
|
15526
|
+
}
|
|
15527
|
+
const getter = findGetterOnPath(pathInfo, getterPaths);
|
|
15528
|
+
if (getter !== null) {
|
|
15529
|
+
raiseError(computedMessage(label, "resetOn", item, getter));
|
|
15530
|
+
}
|
|
15531
|
+
if (source.kind === "path" && item === source.path) {
|
|
15532
|
+
raiseError(`${INVALID} ${label} resetOn "${item}" is the entry's own "from" — every change would reset instead of fold.${LINT_HINT}`);
|
|
15533
|
+
}
|
|
15534
|
+
// 子孫は死に設定: `from` を書くと依存展開で子孫も同じバッチに載り、reset が毎回勝つ。
|
|
15535
|
+
// 祖先(`from: "items.*.qty"` × `resetOn: ["items"]`)は「親の差し替えで作り直す」として通す
|
|
15536
|
+
if (source.kind === "path" && item.startsWith(source.path + DELIMITER)) {
|
|
15537
|
+
raiseError(`${INVALID} ${label} resetOn "${item}" sits under the entry's own "from" "${source.path}" — every write of "from" also lands it, so the reset would win every time and nothing would fold.${LINT_HINT}`);
|
|
15538
|
+
}
|
|
15539
|
+
paths.push(item);
|
|
15540
|
+
}
|
|
15541
|
+
return paths;
|
|
15542
|
+
}
|
|
15543
|
+
/**
|
|
15544
|
+
* scan 出力を介した参照の検査(エントリを跨ぐもの):
|
|
15545
|
+
* - `resetOn` が scan 出力(自他とも)を読む = 累積が累積を消すフィードバック。
|
|
15546
|
+
* - `from` の根を辿って scan 同士が循環する = 互いの書き込みで永久に畳み合う。
|
|
15547
|
+
* 各 scan の `from` は 1 本なので、辿る先は高々 1 つ(関数グラフ)。
|
|
15548
|
+
*/
|
|
15549
|
+
function assertNoOutputReferences(entries) {
|
|
15550
|
+
const outputs = new Set(entries.map((entry) => entry.name));
|
|
15551
|
+
const next = new Map();
|
|
15552
|
+
for (const entry of entries) {
|
|
15553
|
+
for (const path of entry.resetOn) {
|
|
15554
|
+
const root = getPathInfo(path).segments[0];
|
|
15555
|
+
if (outputs.has(root)) {
|
|
15556
|
+
raiseError(`${INVALID} ${entryLabel(entry.name)} resetOn "${path}" reads the ${STATE_SCAN_NAME} output "${root}" — a reset driven by an accumulator is a feedback loop. Reset on the plain inputs instead.${LINT_HINT}`);
|
|
15557
|
+
}
|
|
15558
|
+
}
|
|
15559
|
+
if (entry.source.kind === "path") {
|
|
15560
|
+
const root = entry.source.pathInfo.segments[0];
|
|
15561
|
+
if (outputs.has(root)) {
|
|
15562
|
+
next.set(entry.name, root);
|
|
15563
|
+
}
|
|
15564
|
+
}
|
|
15565
|
+
}
|
|
15566
|
+
for (const start of next.keys()) {
|
|
15567
|
+
const chain = [start];
|
|
15568
|
+
let current = next.get(start);
|
|
15569
|
+
while (typeof current !== "undefined" && chain.length <= outputs.size) {
|
|
15570
|
+
if (current === start) {
|
|
15571
|
+
raiseError(`${INVALID} ${STATE_SCAN_NAME} entries ${[...chain, start].map((n) => `"${n}"`).join(" → ")} feed each other through "from" — each fold would re-trigger the next forever.${LINT_HINT}`);
|
|
15572
|
+
}
|
|
15573
|
+
chain.push(current);
|
|
15574
|
+
current = next.get(current);
|
|
15575
|
+
}
|
|
15576
|
+
}
|
|
15577
|
+
}
|
|
15578
|
+
/**
|
|
15579
|
+
* `$scan` 宣言の検査(`value` だけを読む)。宣言が無い・空なら null。
|
|
15580
|
+
*
|
|
15581
|
+
* getter / setter の判定は `value` の property descriptor から直接取る(`_getterPaths` は
|
|
15582
|
+
* この時点ではまだ旧世代のもの)。`$streams` 名との衝突も `value` の宣言から見る。
|
|
15583
|
+
* `recursion` は `value` から作った `$recursion` のレジストリ(宣言が無ければ null)。
|
|
15584
|
+
*/
|
|
15585
|
+
function parseScanDeclaration(state, eventTokenNames, recursion = null) {
|
|
15586
|
+
const declared = state[STATE_SCAN_NAME];
|
|
15587
|
+
if (typeof declared === "undefined") {
|
|
15588
|
+
return null;
|
|
15589
|
+
}
|
|
15590
|
+
if (typeof declared !== "object" || declared === null || Array.isArray(declared)) {
|
|
15591
|
+
raiseError(`${INVALID} ${STATE_SCAN_NAME} must be an object mapping output names to scan definitions ({ from | on, initial, fold, resetOn? }).${LINT_HINT}`);
|
|
15592
|
+
}
|
|
15593
|
+
const getterPaths = new Set();
|
|
15594
|
+
const setterPaths = new Set();
|
|
15595
|
+
const writeOnlyPaths = new Set();
|
|
15596
|
+
const functionValues = new Map();
|
|
15597
|
+
for (const [key, descriptor] of Object.entries(getAllPropertyDescriptors(state))) {
|
|
15598
|
+
if (typeof descriptor.get === "function") {
|
|
15599
|
+
getterPaths.add(key);
|
|
15600
|
+
}
|
|
15601
|
+
else if (typeof descriptor.set === "function") {
|
|
15602
|
+
writeOnlyPaths.add(key);
|
|
15603
|
+
}
|
|
15604
|
+
if (typeof descriptor.set === "function") {
|
|
15605
|
+
setterPaths.add(key);
|
|
15606
|
+
}
|
|
15607
|
+
if (typeof descriptor.value === "function") {
|
|
15608
|
+
functionValues.set(key, descriptor.value);
|
|
15609
|
+
}
|
|
15610
|
+
}
|
|
15611
|
+
const streamNames = collectStreamNames(state);
|
|
15612
|
+
const entries = [];
|
|
15613
|
+
let order = 0;
|
|
15614
|
+
for (const [name, def] of Object.entries(declared)) {
|
|
15615
|
+
assertOutputName(name, getterPaths, setterPaths, streamNames);
|
|
15616
|
+
// 配列もオブジェクトとして読まない(`$scan` 自体の検査と同じ。静的検証も同じ文言で拾う)
|
|
15617
|
+
if (typeof def !== "object" || def === null || Array.isArray(def)) {
|
|
15618
|
+
raiseError(`${INVALID} ${entryLabel(name)} must be an object ({ from | on, initial, fold, resetOn? }).${LINT_HINT}`);
|
|
15619
|
+
}
|
|
15620
|
+
const definition = def;
|
|
15621
|
+
assertNoMethodConflict(name, functionValues, definition);
|
|
15622
|
+
const source = parseSource(name, definition, eventTokenNames, getterPaths, writeOnlyPaths, recursion);
|
|
15623
|
+
if (!("initial" in definition)) {
|
|
15624
|
+
raiseError(`${INVALID} ${entryLabel(name)} requires "initial" — the seed of the accumulator and the value "resetOn" returns to.${LINT_HINT}`);
|
|
15625
|
+
}
|
|
15626
|
+
if (typeof definition.fold !== "function") {
|
|
15627
|
+
raiseError(`${INVALID} ${entryLabel(name)} fold must be a function.${LINT_HINT}`);
|
|
15628
|
+
}
|
|
15629
|
+
const resetOn = parseResetOn(name, definition.resetOn, source, getterPaths);
|
|
15630
|
+
entries.push({
|
|
15631
|
+
name,
|
|
15632
|
+
source,
|
|
15633
|
+
fold: definition.fold,
|
|
15634
|
+
initial: definition.initial,
|
|
15635
|
+
resetOn,
|
|
15636
|
+
order: order++,
|
|
15637
|
+
});
|
|
15638
|
+
}
|
|
15639
|
+
assertNoOutputReferences(entries);
|
|
15640
|
+
return entries.length > 0 ? entries : null;
|
|
15641
|
+
}
|
|
15642
|
+
/**
|
|
15643
|
+
* 出力の実体化(設計書 D7)。未定義なら `initial` を置き、既に値があれば保持する
|
|
15644
|
+
* (同じオブジェクトの再セット・SSR ハイドレーションで累積を失わない)。plain な配列とオブジェクトは
|
|
15645
|
+
* 複製して置く — 出力が `initial` の間に子パスへ書いても宣言の `initial` を書き換えない(scan/initialValue.ts)。
|
|
15646
|
+
*/
|
|
15647
|
+
function materializeScanOutputs(state, entries) {
|
|
15648
|
+
for (const entry of entries) {
|
|
15649
|
+
if (!(entry.name in state)) {
|
|
15650
|
+
const value = cloneInitial(entry.initial);
|
|
15651
|
+
state[entry.name] = value;
|
|
15652
|
+
recordOutputValue(entry.name, value);
|
|
15653
|
+
}
|
|
15654
|
+
}
|
|
15655
|
+
}
|
|
15656
|
+
function createEventFold(entry) {
|
|
15657
|
+
const fold = entry.fold;
|
|
15658
|
+
return (state, event, ...indexes) => {
|
|
15659
|
+
const proxy = state;
|
|
15660
|
+
// 報告を「出力を読めなかった」「fold が throw した」「出力を書けなかった」で分ける
|
|
15661
|
+
let failure = "read-output";
|
|
15662
|
+
try {
|
|
15663
|
+
const current = proxy[entry.name];
|
|
15664
|
+
failure = "threw";
|
|
15665
|
+
// `resetOn` の書き込みより後の出来事は、reset 後の出力に畳む(scan/eventReset.ts)
|
|
15666
|
+
const reset = hasPendingScanReset(entry);
|
|
15667
|
+
// reset 後の出力から畳む。出力が既に initial と同じ値ならそのまま、違えば複製から(宣言の initial は渡さない)
|
|
15668
|
+
const acc = reset && !isSameAsInitial(current, entry.initial) ? cloneInitial(entry.initial) : current;
|
|
15669
|
+
const next = fold(acc, event, ...indexes);
|
|
15670
|
+
if (isThenable(next)) {
|
|
15671
|
+
// 保留は残す — drain が出力を initial に戻す
|
|
15672
|
+
reportScanThenable(entry.name, next);
|
|
15673
|
+
return;
|
|
15674
|
+
}
|
|
15675
|
+
if (!Object.is(next, current)) {
|
|
15676
|
+
failure = "write";
|
|
15677
|
+
proxy[entry.name] = next;
|
|
15678
|
+
recordOutputValue(entry.name, next);
|
|
15679
|
+
}
|
|
15680
|
+
// 保留は書き込みが通ってから消す。書き込みが throw したら残し、drain が initial に戻す(§2-2)
|
|
15681
|
+
if (reset) {
|
|
15682
|
+
consumePendingScanReset(entry);
|
|
15683
|
+
}
|
|
15684
|
+
}
|
|
15685
|
+
catch (error) {
|
|
15686
|
+
// 後続の subscriber(同じトークンの `$on`)を巻き添えにしない
|
|
15687
|
+
reportScanError(entry.name, error, failure);
|
|
15688
|
+
}
|
|
15689
|
+
};
|
|
15690
|
+
}
|
|
15691
|
+
/**
|
|
15692
|
+
* `on` の scan を event-token の subscriber として登録する(設計書 §2-2)。
|
|
15693
|
+
* 購読の寿命は `$on` と同じ(`_state` の再セットで `clearEventTokenRegistry` の後に張り直す。
|
|
15694
|
+
* 切断では消えないので、ルート `<wcs-state>` の再接続の後も畳み続ける — Issue #273)。
|
|
15695
|
+
*/
|
|
15696
|
+
function subscribeScanEvents(stateElement, entries) {
|
|
15697
|
+
for (const entry of entries) {
|
|
15698
|
+
if (entry.source.kind === "event") {
|
|
15699
|
+
getOrCreateEventToken(stateElement, entry.source.tokenName).subscribe(createEventFold(entry));
|
|
15700
|
+
}
|
|
15701
|
+
}
|
|
15702
|
+
}
|
|
15703
|
+
/**
|
|
15704
|
+
* registry を外す(`_state` 再セットで作り直す前)。旧宣言の `on` scan に立っていた保留 reset は、
|
|
15705
|
+
* 新しい宣言の entry(別のオブジェクト)へ引き継ぐ候補として返し、旧 entry からは消す
|
|
15706
|
+
* (残すと誰にも使われず、保留の数 — scan/eventReset.ts の捨てる走査のゲート — だけが戻らない)。
|
|
15707
|
+
*/
|
|
15708
|
+
function unregisterScans(stateElement) {
|
|
15709
|
+
const carry = new Map();
|
|
15710
|
+
const registry = getScanRegistry(stateElement);
|
|
15711
|
+
if (typeof registry !== "undefined") {
|
|
15712
|
+
for (const entry of registry.entries) {
|
|
15713
|
+
if (consumePendingScanReset(entry)) {
|
|
15714
|
+
carry.set(entry.name, entry.resetOn);
|
|
15715
|
+
}
|
|
15716
|
+
}
|
|
15717
|
+
}
|
|
15718
|
+
clearScanRegistry(stateElement);
|
|
15719
|
+
return carry;
|
|
15720
|
+
}
|
|
15721
|
+
/**
|
|
15722
|
+
* registry を作り、`from` / `resetOn` を依存グラフへ登録する。`from` パスの集合
|
|
15723
|
+
* (旧値キャプチャのゲート)を返す。無ければ null。
|
|
15724
|
+
*
|
|
15725
|
+
* 依存グラフ登録が要る理由は `$watch` と同じ(docs/state-watch-hook-design.md §8):
|
|
15726
|
+
* `setPathInfo` はバインドからしか呼ばれないので、宣言しただけでは祖先への書き込みが
|
|
15727
|
+
* このパスへ展開されず、バッチに載らない。
|
|
15728
|
+
*
|
|
15729
|
+
* `carry`(unregisterScans の戻り値)の保留 reset は、同じ出力名の `on` scan が、まだ drain されていない
|
|
15730
|
+
* 書き込み(updater のキューに積まれているパス)を新旧どちらの `resetOn` にも持つときだけ引き継ぐ(D6)。
|
|
15731
|
+
* 旧宣言の保留は発火対象の state への書き込みでしか立たないので、「enqueue の時点で立つ」契約は保たれる。
|
|
15732
|
+
* 引き継がないと、書き込みより後のイベントが reset されていない出力に畳まれ、drain でも戻らない
|
|
15733
|
+
* (同じバッチの `from` の scan は reset される)。
|
|
15734
|
+
*/
|
|
15735
|
+
function registerScans(stateElement, entries, carry) {
|
|
15736
|
+
const registry = setScanRegistry(stateElement, entries);
|
|
15737
|
+
for (const entry of entries) {
|
|
15738
|
+
const carried = carry.get(entry.name);
|
|
15739
|
+
if (entry.source.kind !== "event" || typeof carried === "undefined") {
|
|
15740
|
+
continue;
|
|
15741
|
+
}
|
|
15742
|
+
if (entry.resetOn.some((path) => carried.includes(path) && getUpdater().hasQueuedPath(stateElement, path))) {
|
|
15743
|
+
markPendingScanReset(entry);
|
|
15744
|
+
}
|
|
15745
|
+
}
|
|
15746
|
+
const fromPaths = new Set();
|
|
15747
|
+
for (const path of registry.byFromPath.keys()) {
|
|
15748
|
+
fromPaths.add(path);
|
|
15749
|
+
stateElement.setPathInfo(path, "prop", "scan");
|
|
15750
|
+
}
|
|
15751
|
+
for (const path of registry.byResetPath.keys()) {
|
|
15752
|
+
stateElement.setPathInfo(path, "prop", "scan");
|
|
15753
|
+
}
|
|
15754
|
+
return fromPaths.size > 0 ? fromPaths : null;
|
|
15755
|
+
}
|
|
15756
|
+
|
|
14053
15757
|
function getterFn(name) {
|
|
14054
15758
|
return function () {
|
|
14055
15759
|
const stateEl = this.stateElement;
|
|
@@ -15303,13 +17007,18 @@ function _getByAddressWithCache(target, address, receiver, handler, stateElement
|
|
|
15303
17007
|
const absPathInfo = getAbsolutePathInfo(stateElement, address.pathInfo);
|
|
15304
17008
|
const absAddress = createAbsoluteStateAddress(absPathInfo, address.listIndex);
|
|
15305
17009
|
const cacheEntry = getCacheEntryByAbsoluteStateAddress(absAddress);
|
|
15306
|
-
|
|
17010
|
+
// 世代印(issue #258 の X10)。絶対アドレスは再セットを跨いで同一なので、dirty だけでは
|
|
17011
|
+
// 旧世代の値と新世代の値を見分けられない。世代の違う項目は単に miss として再評価し、
|
|
17012
|
+
// 下で新しい印を付けて上書きする(列挙も掃き出しも要らず、どのアドレス形状でも自己修復する)。
|
|
17013
|
+
const generation = stateElement.stateGeneration;
|
|
17014
|
+
if (cacheEntry !== null && cacheEntry.dirty === false && cacheEntry.generation === generation) {
|
|
15307
17015
|
return cacheEntry.value;
|
|
15308
17016
|
}
|
|
15309
17017
|
const value = _getByAddress(target, address, receiver, handler, stateElement);
|
|
15310
17018
|
setCacheEntryByAbsoluteStateAddress(absAddress, {
|
|
15311
17019
|
value: value,
|
|
15312
|
-
dirty: false
|
|
17020
|
+
dirty: false,
|
|
17021
|
+
generation: generation
|
|
15313
17022
|
});
|
|
15314
17023
|
return value;
|
|
15315
17024
|
}
|
|
@@ -15428,7 +17137,7 @@ function getListIndexByIndexes(target, receiver, handler, pathInfo, indexes) {
|
|
|
15428
17137
|
const wildcardParentPathInfo = pathInfo.wildcardParentPathInfos[i];
|
|
15429
17138
|
const wildcardAddress = createStateAddress(wildcardParentPathInfo, listIndex);
|
|
15430
17139
|
const tmpValue = getByAddress(target, wildcardAddress, receiver, handler);
|
|
15431
|
-
const listIndexes =
|
|
17140
|
+
const listIndexes = resolveListIndexesByList(tmpValue, listIndex);
|
|
15432
17141
|
if (listIndexes == null) {
|
|
15433
17142
|
raiseError(`ListIndexes not found: ${wildcardParentPathInfo.path}`);
|
|
15434
17143
|
}
|
|
@@ -16094,17 +17803,19 @@ function walkDependency(stateElement, startAddress, staticDependency, dynamicDep
|
|
|
16094
17803
|
* - getter/setter経由のスコープ切り替えも考慮した設計
|
|
16095
17804
|
*/
|
|
16096
17805
|
/**
|
|
16097
|
-
*
|
|
17806
|
+
* 宣言済みパスの `prev` 台帳へ旧値を記録する。台帳を読むのは `$watch`
|
|
17807
|
+
* (docs/state-watch-hook-design.md §4-1)と `$scan` の `from`(docs/state-scan-design.md §2-1)。
|
|
16098
17808
|
*
|
|
16099
|
-
* same-value guard
|
|
16100
|
-
*
|
|
17809
|
+
* same-value guard が既に読んだ旧値だけを使い、そのための追加読みはしない。
|
|
17810
|
+
* どちらも未宣言なら `watchPaths` / `scanPaths` の null 判定 2 個で抜ける(watch 設計書 §10)。
|
|
16101
17811
|
*/
|
|
16102
|
-
function
|
|
16103
|
-
|
|
16104
|
-
if (watchPaths == null || !hasOldValue) {
|
|
17812
|
+
function recordDeclaredPrevValue(stateElement, path, absAddress, oldValue, hasOldValue) {
|
|
17813
|
+
if (!hasOldValue) {
|
|
16105
17814
|
return;
|
|
16106
17815
|
}
|
|
16107
|
-
|
|
17816
|
+
const watchPaths = stateElement.watchPaths;
|
|
17817
|
+
const scanPaths = stateElement.scanPaths;
|
|
17818
|
+
if (watchPaths?.has(path) === true || scanPaths?.has(path) === true) {
|
|
16108
17819
|
recordPrevValue(absAddress, oldValue);
|
|
16109
17820
|
}
|
|
16110
17821
|
}
|
|
@@ -16112,12 +17823,22 @@ function recordWatchPrevValue(stateElement, path, absAddress, oldValue, hasOldVa
|
|
|
16112
17823
|
// binding 経由の書き込みは呼び出し元の dynamic scope から context を引き継ぎ、
|
|
16113
17824
|
// binding 外からの API update は新しい transaction を開始する(設計書 §4 規則 1)。
|
|
16114
17825
|
// 依存 walk で enqueue される派生アドレスも同じ書き込みの因果に属する。
|
|
16115
|
-
function notifyWrite(address, absAddress, receiver, handler, keyedMergePath) {
|
|
17826
|
+
function notifyWrite(address, absAddress, receiver, handler, keyedMergePath, cacheable) {
|
|
16116
17827
|
const propagationContext = config.enablePropagationContext
|
|
16117
17828
|
? (getCurrentPropagationContext() ?? beginPropagationTransaction(-1))
|
|
16118
17829
|
: null;
|
|
16119
17830
|
const updater = getUpdater();
|
|
16120
17831
|
updater.enqueueAbsoluteAddress(absAddress, propagationContext);
|
|
17832
|
+
// 書いたアドレス自身のキャッシュを、依存ウォークより先に無効化する(#274)。ウォークはリスト展開
|
|
17833
|
+
// (list → list.*)と動的依存の親リスト展開で、書いたパスの値を読む。ワイルドカードを含むリストパス
|
|
17834
|
+
// (`groups.*.items`)はキャッシュ対象なので、無効化しないと書き込み前の配列がヒットし、基準との差分が
|
|
17835
|
+
// 「変化なし」になって置き換える前の行だけを展開する(長い配列に置き換えた分の行が着地しない)。
|
|
17836
|
+
// 依存先は下の callback が訪問のたびに読む前に無効化するが、開始アドレスは callback が飛ばす
|
|
17837
|
+
// (二重 enqueue の防止)のでここで済ませる — 開始アドレスも callback で無効化する $postUpdate と同じ順序。
|
|
17838
|
+
// 値は直後の commitWriteCache が載せ直す。
|
|
17839
|
+
if (cacheable) {
|
|
17840
|
+
dirtyCacheEntryByAbsoluteStateAddress(absAddress);
|
|
17841
|
+
}
|
|
16121
17842
|
// 依存関係のあるキャッシュを無効化(ダーティ)、更新対象として登録
|
|
16122
17843
|
walkDependency(handler.stateElement, address, handler.stateElement.staticDependency, handler.stateElement.dynamicDependency, handler.stateElement.listPaths, receiver, "new", (depAddress) => {
|
|
16123
17844
|
// キャッシュを無効化(ダーティ)
|
|
@@ -16158,10 +17879,13 @@ function commitWriteCache(stateElement, path, absAddress, value, cacheable) {
|
|
|
16158
17879
|
}
|
|
16159
17880
|
setCacheEntryByAbsoluteStateAddress(absAddress, {
|
|
16160
17881
|
value: value,
|
|
16161
|
-
dirty: false
|
|
17882
|
+
dirty: false,
|
|
17883
|
+
// 読み側(getByAddress)と同じ世代印を付ける — ヒットになるのは世代が一致する項目だけ
|
|
17884
|
+
// (cache/types.ts の `generation`)。
|
|
17885
|
+
generation: stateElement.stateGeneration
|
|
16162
17886
|
});
|
|
16163
17887
|
}
|
|
16164
|
-
function _setByAddress(target, address, absAddress, value, receiver, handler, keyedMergePath) {
|
|
17888
|
+
function _setByAddress(target, address, absAddress, value, receiver, handler, keyedMergePath, cacheable) {
|
|
16165
17889
|
try {
|
|
16166
17890
|
if (address.pathInfo.path in target) {
|
|
16167
17891
|
if (handler.stateElement.setterPaths.has(address.pathInfo.path)) {
|
|
@@ -16204,10 +17928,10 @@ function _setByAddress(target, address, absAddress, value, receiver, handler, ke
|
|
|
16204
17928
|
}
|
|
16205
17929
|
}
|
|
16206
17930
|
finally {
|
|
16207
|
-
notifyWrite(address, absAddress, receiver, handler, keyedMergePath);
|
|
17931
|
+
notifyWrite(address, absAddress, receiver, handler, keyedMergePath, cacheable);
|
|
16208
17932
|
}
|
|
16209
17933
|
}
|
|
16210
|
-
function _setByAddressWithSwap(target, address, absAddress, value, receiver, handler, keyedMergePath) {
|
|
17934
|
+
function _setByAddressWithSwap(target, address, absAddress, value, receiver, handler, keyedMergePath, cacheable) {
|
|
16211
17935
|
// elementsの場合はswapInfoを準備
|
|
16212
17936
|
let parentAddress = address.parentAddress ?? raiseError(`address.parentAddress is undefined path: ${address.pathInfo.path}`);
|
|
16213
17937
|
let swapInfo = getSwapInfoByAddress(parentAddress);
|
|
@@ -16220,7 +17944,7 @@ function _setByAddressWithSwap(target, address, absAddress, value, receiver, han
|
|
|
16220
17944
|
setSwapInfoByAddress(parentAddress, swapInfo);
|
|
16221
17945
|
}
|
|
16222
17946
|
try {
|
|
16223
|
-
return _setByAddress(target, address, absAddress, value, receiver, handler, keyedMergePath);
|
|
17947
|
+
return _setByAddress(target, address, absAddress, value, receiver, handler, keyedMergePath, cacheable);
|
|
16224
17948
|
}
|
|
16225
17949
|
finally {
|
|
16226
17950
|
const index = swapInfo.value.indexOf(value);
|
|
@@ -16388,7 +18112,7 @@ function setByAddressCore(target, address, value, receiver, handler, keyedMergeP
|
|
|
16388
18112
|
hasOldValue: devHasOldValue,
|
|
16389
18113
|
});
|
|
16390
18114
|
}
|
|
16391
|
-
|
|
18115
|
+
recordDeclaredPrevValue(stateElement, path, absAddress, devOldValue, devHasOldValue);
|
|
16392
18116
|
let dispatchedExport = false;
|
|
16393
18117
|
try {
|
|
16394
18118
|
if (key === undefined) {
|
|
@@ -16409,7 +18133,7 @@ function setByAddressCore(target, address, value, receiver, handler, keyedMergeP
|
|
|
16409
18133
|
return Reflect.set(parentValue, key, value);
|
|
16410
18134
|
}
|
|
16411
18135
|
finally {
|
|
16412
|
-
notifyWrite(address, absAddress, receiver, handler, keyedMergePath);
|
|
18136
|
+
notifyWrite(address, absAddress, receiver, handler, keyedMergePath, cacheable);
|
|
16413
18137
|
if (dispatchedExport) {
|
|
16414
18138
|
// Exported row paths are cacheable but absent from getterPaths. The
|
|
16415
18139
|
// accessor may normalize or reject the input; never pin that input.
|
|
@@ -16454,13 +18178,13 @@ function setByAddressCore(target, address, value, receiver, handler, keyedMergeP
|
|
|
16454
18178
|
hasOldValue: devHasOldValue,
|
|
16455
18179
|
});
|
|
16456
18180
|
}
|
|
16457
|
-
|
|
18181
|
+
recordDeclaredPrevValue(stateElement, path, absAddress, devOldValue, devHasOldValue);
|
|
16458
18182
|
try {
|
|
16459
18183
|
if (isSwappable) {
|
|
16460
|
-
return _setByAddressWithSwap(target, address, absAddress, value, receiver, handler, keyedMergePath);
|
|
18184
|
+
return _setByAddressWithSwap(target, address, absAddress, value, receiver, handler, keyedMergePath, cacheable);
|
|
16461
18185
|
}
|
|
16462
18186
|
else {
|
|
16463
|
-
return _setByAddress(target, address, absAddress, value, receiver, handler, keyedMergePath);
|
|
18187
|
+
return _setByAddress(target, address, absAddress, value, receiver, handler, keyedMergePath, cacheable);
|
|
16464
18188
|
}
|
|
16465
18189
|
}
|
|
16466
18190
|
finally {
|
|
@@ -16912,7 +18636,7 @@ function getAll(target, prop, receiver, handler) {
|
|
|
16912
18636
|
*
|
|
16913
18637
|
* 設計ポイント:
|
|
16914
18638
|
* - ワイルドカードや多重ループ、ネストした配列バインディングに柔軟に対応
|
|
16915
|
-
* -
|
|
18639
|
+
* - resolveListIndexesByListで各階層のリストインデックス集合を取得
|
|
16916
18640
|
* - エラー時はraiseErrorで例外を投げる
|
|
16917
18641
|
*/
|
|
16918
18642
|
function getListIndex(target, resolvedAddress, receiver, handler) {
|
|
@@ -16933,7 +18657,7 @@ function getListIndex(target, resolvedAddress, receiver, handler) {
|
|
|
16933
18657
|
raiseError(`wildcardParentPathInfo is null: ${resolvedAddress.pathInfo.path}`);
|
|
16934
18658
|
const wildcardParentAddress = createStateAddress(wildcardParentPathInfo, parentListIndex);
|
|
16935
18659
|
const wildcardParentValue = getByAddress(target, wildcardParentAddress, receiver, handler);
|
|
16936
|
-
const wildcardParentListIndexes =
|
|
18660
|
+
const wildcardParentListIndexes = resolveListIndexesByList(wildcardParentValue, parentListIndex) ??
|
|
16937
18661
|
raiseError(`ListIndex not found: ${wildcardParentPathInfo.path}`);
|
|
16938
18662
|
const wildcardIndex = resolvedAddress.wildcardIndexes[i] ??
|
|
16939
18663
|
raiseError(`wildcardIndex is null: ${resolvedAddress.pathInfo.path}`);
|
|
@@ -18220,6 +19944,10 @@ function validateVolumeDeclarations(rootStateElement, mountPath, volumeState) {
|
|
|
18220
19944
|
if (typeof volumeState["$streams"] !== "undefined") {
|
|
18221
19945
|
raiseError(`Volume "${mountPath}" declares $streams, which volumes do not support yet. Declare the stream on the root state.`);
|
|
18222
19946
|
}
|
|
19947
|
+
// $scan も未対応(docs/state-scan-design.md D8)。from / on はルートのツリーと token を前提にする
|
|
19948
|
+
if (typeof volumeState["$scan"] !== "undefined") {
|
|
19949
|
+
raiseError(`Volume "${mountPath}" declares $scan, which volumes do not support yet. Declare the scan on the root state.`);
|
|
19950
|
+
}
|
|
18223
19951
|
// $recursion も同じく未対応。宣言だけ受理されたように見えて、どの深さも解決しない
|
|
18224
19952
|
// 状態を作らない(docs/state-recursive-path-impl-plan.md §7)。
|
|
18225
19953
|
if (typeof volumeState[STATE_RECURSION_NAME] !== "undefined") {
|
|
@@ -18476,6 +20204,20 @@ class State extends HTMLElementBase {
|
|
|
18476
20204
|
// 静的子展開はこの集合の subtree に限定される。追加のみ・クリアしない(安全側)。
|
|
18477
20205
|
_indexDependentGetterPaths = new Set();
|
|
18478
20206
|
_initialized = false;
|
|
20207
|
+
/**
|
|
20208
|
+
* 初期化(`_initialize`)が失敗した(#257)。`_initialized` の裏返しではない —
|
|
20209
|
+
* 「まだ初期化していない」と「もう初期化できない」を取り違えると、復旧不能の
|
|
20210
|
+
* 要素に `setInitialState` が効いたように見える。真にするのは
|
|
20211
|
+
* `_failInitializeLoudly` だけ。
|
|
20212
|
+
*/
|
|
20213
|
+
_initializeFailed = false;
|
|
20214
|
+
/**
|
|
20215
|
+
* この接続サイクルの失敗は**もう着地した**(#257)。設定エラーの fail-fast
|
|
20216
|
+
* (`_failInitialization` と `initializeMountScope` の catch)は自分で promise を
|
|
20217
|
+
* 解決してから raise する — connectedCallbackPromise を**解決**する側のクラスなので、
|
|
20218
|
+
* 下の loud な着地(reject + 診断)に載せ替えると意味論が変わる。接続ごとに畳む。
|
|
20219
|
+
*/
|
|
20220
|
+
_initializationLanded = false;
|
|
18479
20221
|
_initializePromise;
|
|
18480
20222
|
_resolveInitialize = null;
|
|
18481
20223
|
_connectedCallbackPromise;
|
|
@@ -18496,8 +20238,40 @@ class State extends HTMLElementBase {
|
|
|
18496
20238
|
_dynamicDependency = new Map();
|
|
18497
20239
|
_staticDependency = new Map();
|
|
18498
20240
|
_pathSet = new Set();
|
|
20241
|
+
/**
|
|
20242
|
+
* state の世代(issue #258 の X10)。`_state` の差し替えごとに 1 つ進み、キャッシュ項目の
|
|
20243
|
+
* 印になる(cache/types.ts の `generation`)。`_version`(更新サイクルの番号)とは別の
|
|
20244
|
+
* カウンタで、増える条件が違う — 再セットは世代だけを進める
|
|
20245
|
+
* (`__tests__/integration.stateGenerationReset.test.ts` の
|
|
20246
|
+
* 「再セットは世代だけを進め、version は動かさない」が固定する)。
|
|
20247
|
+
*/
|
|
20248
|
+
_stateGeneration = 0;
|
|
20249
|
+
/**
|
|
20250
|
+
* この要素が受け取った `setPathInfo` の台帳(issue #258 の X7)。パス → 種別と呼び出し元。
|
|
20251
|
+
*
|
|
20252
|
+
* `_pathSet` / `_listPaths` / `_elementPaths` は再セットでクリアされるが、それらを登録した
|
|
20253
|
+
* バインドは生き残る(再セットは DOM を作り直さない)。台帳が空のままだと依存ウォークが
|
|
20254
|
+
* `items` をリストとして展開できず、全リスト書き込みが「非リストのアドレスにワイルドカードを
|
|
20255
|
+
* 展開できない」で恒久的に throw する(値と DOM は追従するが、書き込みのたびに投げ続ける)。
|
|
20256
|
+
* クリアのあと、この台帳から作り直す(`_rebuildPathInfo`)。
|
|
20257
|
+
*
|
|
20258
|
+
* `source === "internal"`(再帰の生成アクセサ・ボリュームのツリーアクセサ)は載せない。
|
|
20259
|
+
* あれらは生やした機構が新しい世代で登録し直す(recursion/registry.ts の `_define`)。
|
|
20260
|
+
*/
|
|
20261
|
+
_pathRegistrations = new Map();
|
|
20262
|
+
/**
|
|
20263
|
+
* これまでのどの世代かで再帰レジストリが実体化した具体パス(`nodes.*.total` 等)の累積。
|
|
20264
|
+
* 再セットのたびに `forgetGenerated` の戻り値を足し、`_rebuildPathInfo` の除外に使う。
|
|
20265
|
+
*
|
|
20266
|
+
* 除外は**世代を跨いで累積する**。読みを挟まない連続した再セットでも、生成アクセサの具体パスを
|
|
20267
|
+
* 指す静的辺(`nodes.*` → `nodes.*.total`)が戻らないことは、
|
|
20268
|
+
* `__tests__/integration.stateGenerationReset.test.ts` の
|
|
20269
|
+
* 「読みを挟まない 3 連続の再セットで静的辺が戻らない」が固定する。
|
|
20270
|
+
*/
|
|
20271
|
+
_generatedPaths = new Set();
|
|
18499
20272
|
// `$watch` 宣言の監視対象パス。宣言が無ければ null(setByAddress のゼロコスト契約)
|
|
18500
20273
|
_watchPaths = null;
|
|
20274
|
+
_scanPaths = null;
|
|
18501
20275
|
_version = 0;
|
|
18502
20276
|
_rootNode = null;
|
|
18503
20277
|
_boundComponent = null;
|
|
@@ -18549,26 +20323,53 @@ class State extends HTMLElementBase {
|
|
|
18549
20323
|
set _state(value) {
|
|
18550
20324
|
// 旧世代のデータ。再帰の生成物(辺・キャッシュ)を忘れるとき、台帳を辿る起点になる
|
|
18551
20325
|
const previousState = this.__state;
|
|
18552
|
-
// 順序:
|
|
18553
|
-
//
|
|
18554
|
-
//
|
|
18555
|
-
//
|
|
18556
|
-
//
|
|
18557
|
-
//
|
|
18558
|
-
//
|
|
20326
|
+
// 順序: **`value` しか読まない検証** → 旧世代の後始末 → 世代を進める → 差し替え → 再収集。
|
|
20327
|
+
//
|
|
20328
|
+
// 宣言の検証は世代更新を挟んで 2 群に割れる。
|
|
20329
|
+
// - 世代を進める**前**に走る 5 つ: `$recursion`(宣言とレジストリの構築)・`$commandTokens`・
|
|
20330
|
+
// `$eventTokens`・`$listKeys`・`$scan`。どれも `value` しか読まないのでここへ置ける。この 5 つで
|
|
20331
|
+
// throw した再セットは世代を進めず、`__state` も旧世代の読みもそのまま残る。
|
|
20332
|
+
// - 世代を進めた**後**に走る 3 つ: `$on`(下・`_eventTokenNames` と token registry を要る)・
|
|
20333
|
+
// `$streams`・`$watch`。この 3 つで throw した再セットは、世代が進んで `__state` も
|
|
20334
|
+
// 新しくなった後に落ちる。
|
|
20335
|
+
// throw の後に何が残るかは検証ごとに違う。散文で言い直さない —
|
|
20336
|
+
// `__tests__/integration.stateGenerationReset.test.ts` が 1 つずつ固定する。
|
|
20337
|
+
//
|
|
20338
|
+
// 後の 2 つがこの位置に要る理由(同じファイルが 1 本ずつ固定する):
|
|
20339
|
+
// - `$streams` の衝突検査は**新しい** value から集め直した `getterPaths` / `setterPaths` を
|
|
20340
|
+
// 見る(旧 state だけが持つ getter は、新 state の同名 `$streams` と衝突しない)。
|
|
20341
|
+
// - `$watch` の存在検査は**新しい** `__state` を見る(新 state にだけあるパスの watch は
|
|
20342
|
+
// `wcs/watch-path-missing` にならない)。
|
|
18559
20343
|
const recursionSpec = processRecursionDeclaration(value);
|
|
18560
20344
|
const recursionRegistry = recursionSpec === null ? null : new RecursionRegistry(recursionSpec, value);
|
|
18561
20345
|
const commandTokenNames = processCommandTokensDeclaration(value);
|
|
18562
20346
|
const eventTokenNames = processEventTokensDeclaration(value);
|
|
20347
|
+
// $listKeys の検証は `value` しか読まない(要素にも旧世代にも触れない)ので、ここで済ませる。
|
|
20348
|
+
// 反映は下の所定位置のまま(クリアと再収集の並びは変えない)。
|
|
20349
|
+
const listKeys = processListKeysDeclaration(value);
|
|
20350
|
+
// $scan の検証も `value` と宣言済みトークン名しか読まない(docs/state-scan-design.md §1-2)。
|
|
20351
|
+
// `**` getter の展開形を from に書いた形を落とすため、`value` から作った再帰レジストリも渡す(D5)。
|
|
20352
|
+
// fold が関数を返す出力は、その出力に置いた関数値をメソッド衝突と見なさない(scan/initialValue.ts の記録・D7)。
|
|
20353
|
+
const scanEntries = parseScanDeclaration(value, eventTokenNames, recursionRegistry);
|
|
18563
20354
|
// 旧世代の生成アクセサ(own)・それを指す依存辺・評価結果のキャッシュを忘れてから
|
|
18564
20355
|
// 差し替える(recursion/generation.ts)。own の生成アクセサは、同じオブジェクトを再セットする
|
|
18565
20356
|
// ときに下の `getStateInfo` が `getterPaths` へ拾い直す前に消えていなければならない。
|
|
20357
|
+
// 前世代の再帰レジストリが生やした具体パスは `_generatedPaths` に積む。経路情報の作り直し
|
|
20358
|
+
// (`_rebuildPathInfo`)はそこを除く — 新しい世代ではまだ実体化されていないため。除外が
|
|
20359
|
+
// 世代を跨いで累積することは `_generatedPaths` の注記(と、そこが挙げるテスト)。
|
|
18566
20360
|
if (this._recursionRegistry !== null) {
|
|
18567
|
-
this._recursionRegistry.forgetGenerated(this, previousState)
|
|
20361
|
+
for (const path of this._recursionRegistry.forgetGenerated(this, previousState)) {
|
|
20362
|
+
this._generatedPaths.add(path);
|
|
20363
|
+
}
|
|
18568
20364
|
}
|
|
18569
20365
|
this._recursionRegistry = recursionRegistry;
|
|
18570
20366
|
this._commandTokenNames = commandTokenNames;
|
|
18571
20367
|
this._eventTokenNames = eventTokenNames;
|
|
20368
|
+
// 世代を進める(issue #258 の X10)。位置は「旧世代の後始末(forgetGenerated)の**後**・
|
|
20369
|
+
// `__state` の差し替えの**前**」。上の 5 つ(`value` しか読まない検証)で throw した再セットは
|
|
20370
|
+
// ここへ到達しないので、要素は丸ごと旧世代に留まる — 世代も、旧世代のキャッシュが返す読みも
|
|
20371
|
+
// 据え置き(同ファイルの「throw する再セットは世代を進めない」4 本が固定する)。
|
|
20372
|
+
this._stateGeneration++;
|
|
18572
20373
|
this.__state = value;
|
|
18573
20374
|
// $updatedCallback の有無を state セット時に確定しておく(in はプロトタイプ
|
|
18574
20375
|
// チェーンも見る・getter を評価しない)。drain 側はこのフラグで更新アドレスの
|
|
@@ -18580,6 +20381,12 @@ class State extends HTMLElementBase {
|
|
|
18580
20381
|
this._hasErrorCallback = STATE_ERROR_CALLBACK_NAME in value;
|
|
18581
20382
|
// 再 set 時に二重 subscribe しないよう registry をクリアしてから $on を配線し直す。
|
|
18582
20383
|
clearEventTokenRegistry(this);
|
|
20384
|
+
// $scan(docs/state-scan-design.md §2-4): 出力の実体化は `_rebuildPathInfo` より前、
|
|
20385
|
+
// `on` の購読は `$on` より前(同じトークンでは reducer → effect の順、D11)。
|
|
20386
|
+
if (scanEntries !== null) {
|
|
20387
|
+
materializeScanOutputs(value, scanEntries);
|
|
20388
|
+
subscribeScanEvents(this, scanEntries);
|
|
20389
|
+
}
|
|
18583
20390
|
processOnDeclaration(this, value, this._eventTokenNames);
|
|
18584
20391
|
this._listPaths.clear();
|
|
18585
20392
|
this._elementPaths.clear();
|
|
@@ -18604,7 +20411,7 @@ class State extends HTMLElementBase {
|
|
|
18604
20411
|
processStreamsDeclaration(this, value);
|
|
18605
20412
|
// $listKeys: 宣言が無ければ null のままで、setByAddress のキー突合経路には
|
|
18606
20413
|
// 一切入らない(docs/state-list-key-design.md §7-1)。再 set で必ず置き換える。
|
|
18607
|
-
this._listKeys =
|
|
20414
|
+
this._listKeys = listKeys;
|
|
18608
20415
|
// $recursion: 宣言が無ければ null のままで、読みのホットパスには一切入らない。
|
|
18609
20416
|
// レジストリの構築・旧世代の後始末・差し替えはセッタの先頭で済んでいる(`__state` の
|
|
18610
20417
|
// 差し替え前)。ここに残るのはリストパスの登録だけ(`_listPaths.clear()` の後であること)。
|
|
@@ -18621,6 +20428,15 @@ class State extends HTMLElementBase {
|
|
|
18621
20428
|
// 乖離したまま自己回復しない)。
|
|
18622
20429
|
this._listPaths.add(recursionSpec.anchorList);
|
|
18623
20430
|
}
|
|
20431
|
+
// 生きているバインドの経路情報を作り直す(issue #258 の X7)。置き場所は
|
|
20432
|
+
// `_pathSet` / `_listPaths` / `_elementPaths` のクリアより後、startWatch / startStreams より前。
|
|
20433
|
+
// `setPathInfo` をやり直しても新しい診断は出ない — 第 2 世代で消えたバインド先が無言のまま
|
|
20434
|
+
// であることを `__tests__/integration.stateGenerationReset.test.ts` の末尾が固定する。
|
|
20435
|
+
this._rebuildPathInfo();
|
|
20436
|
+
// $scan: registry と from / resetOn の依存グラフ登録(_pathSet クリア後であること)。
|
|
20437
|
+
// startWatch より前に置く(scan だけを宣言した state も drain の発火対象に載せるため)。
|
|
20438
|
+
const carriedScanResets = unregisterScans(this);
|
|
20439
|
+
this._scanPaths = scanEntries === null ? null : registerScans(this, scanEntries, carriedScanResets);
|
|
18624
20440
|
// $watch: 旧宣言のハンドラが残らないよう registry を落としてから新宣言を解析する。
|
|
18625
20441
|
// _pathSet.clear() の後であること(依存グラフ登録をやり直す必要がある、
|
|
18626
20442
|
// docs/state-watch-hook-design.md §8)。宣言が無ければ watchPaths は null で、
|
|
@@ -18736,7 +20552,12 @@ class State extends HTMLElementBase {
|
|
|
18736
20552
|
}
|
|
18737
20553
|
catch (error) {
|
|
18738
20554
|
// 設定エラーでも初期化待ちをウェッジさせない(_failInitialization と同じ規範 —
|
|
18739
|
-
// 未解決のまま投げると waitForStateInitialize
|
|
20555
|
+
// 未解決のまま投げると waitForStateInitialize がページ全体を無言で止める)。
|
|
20556
|
+
// ここを `_failInitializeLoudly` に載せないのは意図(#257 第 3 ラウンド):
|
|
20557
|
+
// この要素はルートではないので、あちらの「この rootNode にルートは来ない」着地
|
|
20558
|
+
//(markBindingsUnavailable / failPendingVolumes)が**無関係なルートと兄弟
|
|
20559
|
+
// ボリュームを巻き添えにする**。promise を解決してから raise する点は
|
|
20560
|
+
// `name=` と同じクラスで、reject 側へ動かすのは別の設計判断
|
|
18740
20561
|
this._resolveInitialize?.();
|
|
18741
20562
|
this._resolveLoading?.();
|
|
18742
20563
|
this._resolveConnectedCallback?.();
|
|
@@ -18779,6 +20600,10 @@ class State extends HTMLElementBase {
|
|
|
18779
20600
|
}
|
|
18780
20601
|
graftOrQueueVolume(rootNode, getStateElement(rootNode), mountPath, volumeState, finish);
|
|
18781
20602
|
}
|
|
20603
|
+
/**
|
|
20604
|
+
* 初回マウントのロードと登録。戻り値は「この接続で初期化を**完了**したか」で、
|
|
20605
|
+
* `false` は失敗ではなく**中断**(ロード中に要素が剥がされた — 下の注記)。
|
|
20606
|
+
*/
|
|
18782
20607
|
async _initialize() {
|
|
18783
20608
|
// enable-ssr (クライアント側のみ): <wcs-ssr> から初期データを取得
|
|
18784
20609
|
const ssrState = !inSsr() ? this._loadFromSsrElement() : null;
|
|
@@ -18803,7 +20628,21 @@ class State extends HTMLElementBase {
|
|
|
18803
20628
|
}
|
|
18804
20629
|
}
|
|
18805
20630
|
await this._loadingPromise;
|
|
18806
|
-
|
|
20631
|
+
if (this._rootNode === null) {
|
|
20632
|
+
// ロード中に剥がされた(行プールの張り直し・DOM の移動・shadow の組み直し)。
|
|
20633
|
+
// これは初期化の**失敗ではなく中断**である: 作者のミスは 1 つも無く、state も
|
|
20634
|
+
// 健全。診断を出さず、要素を毒化せず(_initializeFailed を立てず)、
|
|
20635
|
+
// connectedCallbackPromise も拒否せずに、この接続だけを黙って終わらせる。
|
|
20636
|
+
//
|
|
20637
|
+
// 拒否してはいけない理由は取り返しのつかなさ: promise はコンストラクタで
|
|
20638
|
+
// 1 度だけ作られるので、一度拒否すると**付け直して正常に初期化できた要素**まで
|
|
20639
|
+
// 永久に「失敗」を報告し続け、それを待つ @wcstack/server の renderToString と
|
|
20640
|
+
// @wcstack/testing の mount が健全なページで throw する。
|
|
20641
|
+
// 付け直した接続が改めてここへ来て、通常どおり登録と解決を行う。
|
|
20642
|
+
return false;
|
|
20643
|
+
}
|
|
20644
|
+
setStateElement(this._rootNode, this);
|
|
20645
|
+
return true;
|
|
18807
20646
|
}
|
|
18808
20647
|
/**
|
|
18809
20648
|
* 設定エラーでの fail-fast。initializePromise 等を解決してから raise する —
|
|
@@ -18813,6 +20652,8 @@ class State extends HTMLElementBase {
|
|
|
18813
20652
|
* として loud に残る。
|
|
18814
20653
|
*/
|
|
18815
20654
|
_failInitialization(message) {
|
|
20655
|
+
// 着地はここで完了(connectedCallback の catch が二重に着地させない — #257)
|
|
20656
|
+
this._initializationLanded = true;
|
|
18816
20657
|
// _initialized は立てない — 切断時の後始末(createState を要する)が
|
|
18817
20658
|
// 未ロードの state を触らないよう、初期化前ガードに掛かるままにする
|
|
18818
20659
|
this._resolveInitialize?.();
|
|
@@ -18820,6 +20661,83 @@ class State extends HTMLElementBase {
|
|
|
18820
20661
|
this._resolveConnectedCallback?.();
|
|
18821
20662
|
raiseError(message);
|
|
18822
20663
|
}
|
|
20664
|
+
/**
|
|
20665
|
+
* `_initialize` の失敗の着地(#257)。旧挙動は「throw が connectedCallback の外へ
|
|
20666
|
+
* 出るだけ」で、`_initializePromise` も `_connectedCallbackPromise` も永久に未解決の
|
|
20667
|
+
* まま残り、作者が受け取るのは診断ではなく無言のハングだった。載るのは
|
|
20668
|
+
* `_initialize` が投げうるもの全部 — `_state` セッタの宣言検証($recursion /
|
|
20669
|
+
* $commandTokens / $eventTokens / $on / $streams / $listKeys / $watch)、
|
|
20670
|
+
* `_loadStateFromSource` のロード失敗(src の拡張子・json のパース・内包スクリプト・
|
|
20671
|
+
* 外部モジュール)、SSR データの merge、そして `setStateElement` の
|
|
20672
|
+
* 「1 rootNode 1 ツリー」違反(**別の**要素が 2 本目に来た形 — 同じ要素の再登録は
|
|
20673
|
+
* 冪等なので、ロード中の remove → append はここへ来ない)。
|
|
20674
|
+
*
|
|
20675
|
+
* 載**らない**もの: ロード中に要素が剥がされた形。作者のミスが 1 つも無いので
|
|
20676
|
+
* 初期化失敗ではなく中断として扱う(`_initialize` が `false` を返す)。
|
|
20677
|
+
*
|
|
20678
|
+
* もう 1 つ載らないのがボリューム(`mount=`)の失敗。`_initializeVolume` の catch が
|
|
20679
|
+
* 3 つの promise(initialize / loading / connectedCallback)を自分で解決してから raise し、
|
|
20680
|
+
* `connectedCallback` のボリューム分岐はそれを包まないので、ボリュームはこの着地に
|
|
20681
|
+
* 載らず connectedCallbackPromise を**拒否しない**。自前の報告が出るかどうかは失敗の
|
|
20682
|
+
* 種類による。報告が無い形では、逃げ方は `name=`(`_failInitialization` の注記)と
|
|
20683
|
+
* 同じで、throw はカスタム要素リアクションが捨てる戻り Promise へ出ていく
|
|
20684
|
+
* (ブラウザのコンソールには "Uncaught (in promise)" として残るが、promise を待つ側
|
|
20685
|
+
* = renderToString・mount・テストレシピには届かない)。失敗箇所ごとの正確な挙動は
|
|
20686
|
+
* `__tests__/integration.initFailureDiagnostics.test.ts` が固定している。挙動はこの
|
|
20687
|
+
* PR では変えない(枠の寿命は別 Issue)。
|
|
20688
|
+
*
|
|
20689
|
+
* `connectedCallback` が `_initialize` より前に await する 2 つ
|
|
20690
|
+
* (`_initializeDCC` / `_initializeBindWebComponent`)の raise も同じ着地に載る。
|
|
20691
|
+
* 特に「初期化に失敗した要素の再接続」は `bindWebComponent` → `setInitialState` の
|
|
20692
|
+
* 復旧不能 raise でそこへ来るので、包まないと診断ゼロで素通りする。
|
|
20693
|
+
*
|
|
20694
|
+
* `_failInitialization`(設定エラーの fail-fast)との違いは 1 つ:
|
|
20695
|
+
* **connectedCallbackPromise を reject する**。ここまで来た要素は state を 1 つも
|
|
20696
|
+
* 持たない = このツリーは存在しない。resolve すると、それを待つ消費者
|
|
20697
|
+
* (@wcstack/server の renderToString・@wcstack/testing の mount・README の
|
|
20698
|
+
* テストレシピ)に「準備完了」と嘘をつく。下の SSR 経路(_rejectConnectedCallback)と
|
|
20699
|
+
* 同じ規範で、そちらと同じく**元のエラーをそのまま**投げ直す。
|
|
20700
|
+
*
|
|
20701
|
+
* `initializePromise` は従来どおり**解決**する(reject しない)。
|
|
20702
|
+
* `waitForStateInitialize` はページ中の全 `<wcs-state>` の initializePromise を
|
|
20703
|
+
* `Promise.all` で待つので、reject にすると 1 要素の設定ミスが無関係な
|
|
20704
|
+
* バインディングまで道連れになる(_failInitialization の注記と同じ理由)。
|
|
20705
|
+
*
|
|
20706
|
+
* 後始末はしない: `_initialized` を立てないので `disconnectedCallback` は初期化前
|
|
20707
|
+
* ガードで抜ける。`$listKeys` / `$watch` のようにセッタの後半で落ちた形では
|
|
20708
|
+
* `$on` の購読と stream registry が残るが、この要素は復旧不能(setInitialState が
|
|
20709
|
+
* throw する)なので、残骸は要素ごと捨てる前提で放置する。
|
|
20710
|
+
*/
|
|
20711
|
+
_failInitializeLoudly(error) {
|
|
20712
|
+
if (this._initializationLanded) {
|
|
20713
|
+
// 設定エラーの fail-fast が自分で着地済み(promise は解決済み)。ここで
|
|
20714
|
+
// reject に載せ替えると「ページの残りは生きている設定ミス」と「state を 1 つも
|
|
20715
|
+
// 持たない要素」を混ぜることになるので、伝播だけさせる
|
|
20716
|
+
throw error;
|
|
20717
|
+
}
|
|
20718
|
+
this._initializeFailed = true;
|
|
20719
|
+
// 診断は必ず 1 件出す。カスタム要素リアクションは connectedCallback の戻り
|
|
20720
|
+
// Promise を捨てるので、ブラウザの "Uncaught (in promise)" 以外に受け手が居ない
|
|
20721
|
+
console.error(`[@wcstack/state] <${config.tagNames.state}> failed to initialize.`, error);
|
|
20722
|
+
this._resolveInitialize?.();
|
|
20723
|
+
this._resolveLoading?.();
|
|
20724
|
+
// reject より先に handled を立てる。DOM 駆動のマウントでは
|
|
20725
|
+
// connectedCallbackPromise を誰も await しないため、印が無いと
|
|
20726
|
+
// unhandled rejection になる(Node ではプロセスごと落ちる)
|
|
20727
|
+
this._connectedCallbackPromise.catch(() => undefined);
|
|
20728
|
+
this._rejectConnectedCallback?.(error);
|
|
20729
|
+
// このツリーは存在しない。ready を即時解決のまま残すと waitForReady
|
|
20730
|
+
// (@wcstack/server)が「バインド構築済み」と報告し、保留中のボリュームは
|
|
20731
|
+
// ルート登録が来ないので永久に未解決のまま残る。
|
|
20732
|
+
// 生きたルートが既にこの rootNode に居る形(2 本目の <wcs-state> = v2 の
|
|
20733
|
+
// 「1 rootNode 1 ツリー」違反)では、ページは 1 本目で成立している —
|
|
20734
|
+
// ready も保留ボリュームも 1 本目のものなので触らない
|
|
20735
|
+
if (this._rootNode !== null && getStateElement(this._rootNode) === null) {
|
|
20736
|
+
markBindingsUnavailable(this._rootNode, error);
|
|
20737
|
+
failPendingVolumes(this._rootNode);
|
|
20738
|
+
}
|
|
20739
|
+
throw error;
|
|
20740
|
+
}
|
|
18823
20741
|
async _initializeBindWebComponent() {
|
|
18824
20742
|
if (this.hasAttribute("bind-component")) {
|
|
18825
20743
|
// wcs-stateはコンポーネントのトップレベル要素であること
|
|
@@ -18940,6 +20858,8 @@ class State extends HTMLElementBase {
|
|
|
18940
20858
|
initializeMountScope(record, parentNode instanceof ShadowRoot ? parentNode : boundComponent);
|
|
18941
20859
|
}
|
|
18942
20860
|
catch (error) {
|
|
20861
|
+
// 着地はここで完了(_failInitialization と同じクラス — #257)
|
|
20862
|
+
this._initializationLanded = true;
|
|
18943
20863
|
this._resolveInitialize?.();
|
|
18944
20864
|
this._resolveLoading?.();
|
|
18945
20865
|
this._resolveConnectedCallback?.();
|
|
@@ -19020,6 +20940,8 @@ class State extends HTMLElementBase {
|
|
|
19020
20940
|
// 再開からの起動を防ぐ)。前回接続中の再 set(S13)で立った
|
|
19021
20941
|
// _streamsStartedGeneration も世代不一致となり自然に無効化される。
|
|
19022
20942
|
const connectGeneration = ++this._connectGeneration;
|
|
20943
|
+
// 着地の印は接続ごとに畳む(前の接続の fail-fast をこの接続へ持ち越さない — #257)
|
|
20944
|
+
this._initializationLanded = false;
|
|
19023
20945
|
if (!this._initialized) {
|
|
19024
20946
|
// 名前次元は v2 で撤去(D16 / §9)。名前付き State はボリュームへ移行する。
|
|
19025
20947
|
// mount 併記(移行途中で name を残した形)は専用文言で誘導する
|
|
@@ -19034,14 +20956,21 @@ class State extends HTMLElementBase {
|
|
|
19034
20956
|
const parentNode = this.parentNode;
|
|
19035
20957
|
if (parentNode instanceof ShadowRoot &&
|
|
19036
20958
|
parentNode.host.hasAttribute(DCC_DEFINITION_ATTRIBUTE)) {
|
|
19037
|
-
|
|
19038
|
-
|
|
19039
|
-
|
|
19040
|
-
|
|
19041
|
-
|
|
19042
|
-
|
|
20959
|
+
try {
|
|
20960
|
+
// DCC と bind-component は排他。DCC の state はテンプレートに属し、
|
|
20961
|
+
// インスタンスごとにロードされるので、定義時点のホストのプロパティを
|
|
20962
|
+
// ソースにする bind-component とは両立しない。従来はこの return で
|
|
20963
|
+
// 無言に無視していた(docs/architecture-hardening/15 §3.1)。
|
|
20964
|
+
if (this.hasAttribute("bind-component")) {
|
|
20965
|
+
raiseError(`"bind-component" cannot be used inside a [${DCC_DEFINITION_ATTRIBUTE}] host. DCC state comes from the template, not from a component property.`);
|
|
20966
|
+
}
|
|
20967
|
+
await this._initializeDCC(parentNode.host, parentNode);
|
|
20968
|
+
}
|
|
20969
|
+
catch (error) {
|
|
20970
|
+
// _initialize と同じ着地(#257)。DCC のロード失敗もここまでは
|
|
20971
|
+
// 「throw が connectedCallback の外へ出るだけ」= 無言のハングだった
|
|
20972
|
+
this._failInitializeLoudly(error);
|
|
19043
20973
|
}
|
|
19044
|
-
await this._initializeDCC(parentNode.host, parentNode);
|
|
19045
20974
|
return;
|
|
19046
20975
|
}
|
|
19047
20976
|
// ボリューム(`mount="path"` — 接ぎ木・docs/state-mount-design.md §4-2)
|
|
@@ -19056,7 +20985,17 @@ class State extends HTMLElementBase {
|
|
|
19056
20985
|
await this._initializeVolume();
|
|
19057
20986
|
return;
|
|
19058
20987
|
}
|
|
19059
|
-
|
|
20988
|
+
try {
|
|
20989
|
+
await this._initializeBindWebComponent();
|
|
20990
|
+
}
|
|
20991
|
+
catch (error) {
|
|
20992
|
+
// bind-component の raise も同じ着地に載せる(#257)。とりわけ「初期化に
|
|
20993
|
+
// 失敗した要素の再接続」は bindWebComponent → setInitialState の復旧不能
|
|
20994
|
+
// raise でここへ来る — _initialize の外なので、包まないと素通りする。
|
|
20995
|
+
// 自分で着地済みの fail-fast(_failInitialization / initializeMountScope)は
|
|
20996
|
+
// _failInitializeLoudly の先頭で弾かれ、従来どおり伝播するだけ
|
|
20997
|
+
this._failInitializeLoudly(error);
|
|
20998
|
+
}
|
|
19060
20999
|
if (this._mountRecord !== null) {
|
|
19061
21000
|
// v2 マウント: この要素は独立ツリーを持たない(台帳エイリアスが親を指す)。
|
|
19062
21001
|
// 名前登録・state ロード・$connectedCallback / $watch / $streams は行わない
|
|
@@ -19067,7 +21006,20 @@ class State extends HTMLElementBase {
|
|
|
19067
21006
|
this._resolveConnectedCallback?.();
|
|
19068
21007
|
return;
|
|
19069
21008
|
}
|
|
19070
|
-
|
|
21009
|
+
let completed = false;
|
|
21010
|
+
try {
|
|
21011
|
+
completed = await this._initialize();
|
|
21012
|
+
}
|
|
21013
|
+
catch (error) {
|
|
21014
|
+
// ここが唯一の無防備な await だった(#257)。throw は下の 2 行と
|
|
21015
|
+
// 末尾の _resolveConnectedCallback を飛ばし、両 promise を永久未解決にする
|
|
21016
|
+
this._failInitializeLoudly(error);
|
|
21017
|
+
}
|
|
21018
|
+
if (!completed) {
|
|
21019
|
+
// ロード中に剥がされた = 失敗ではなく中断(_initialize の注記)。promise は
|
|
21020
|
+
// 未解決のまま残し、付け直した接続にそのまま解決させる
|
|
21021
|
+
return;
|
|
21022
|
+
}
|
|
19071
21023
|
this._initialized = true;
|
|
19072
21024
|
this._resolveInitialize?.();
|
|
19073
21025
|
}
|
|
@@ -19114,7 +21066,13 @@ class State extends HTMLElementBase {
|
|
|
19114
21066
|
}
|
|
19115
21067
|
// enable-ssr (クライアント側): SSR で $connectedCallback 済みなのでスキップ
|
|
19116
21068
|
// inSsr() (サーバー側): レンダリング中なので実行する
|
|
19117
|
-
|
|
21069
|
+
// 世代ガード(connectGeneration 照合): ロード完了前の remove → append では
|
|
21070
|
+
// _initialize が 2 本同時に走り、**負けたほうもここまで到達する**(登録は冪等に
|
|
21071
|
+
// 弾かれるだけで tail は止まらない)。ガードが無いと作者の $connectedCallback が
|
|
21072
|
+
// 1 接続につき 2 回走り、副作用も 2 回出る。下の startWatch / startStreams と
|
|
21073
|
+
// 同じ規範で、起動点を最新の connect に一本化する
|
|
21074
|
+
if ((!this.hasAttribute('enable-ssr') || inSsr())
|
|
21075
|
+
&& connectGeneration === this._connectGeneration) {
|
|
19118
21076
|
await this._callStateConnectedCallback();
|
|
19119
21077
|
}
|
|
19120
21078
|
// サーバーモード + enable-ssr: バインディング完了後に <wcs-ssr> を生成。
|
|
@@ -19204,6 +21162,16 @@ class State extends HTMLElementBase {
|
|
|
19204
21162
|
// 初期化前に剥がされた(bind-component の await 中に shadow が張り直された等)。
|
|
19205
21163
|
// 名前登録も token も stream もまだ無く、state も作れないので後始末は不要。
|
|
19206
21164
|
// ここで createState すると "_state is not initialized" で CE リアクションが落ちる
|
|
21165
|
+
if (this._initializeFailed) {
|
|
21166
|
+
// 落ちたルート要素**本人**が DOM から消えた =「このルートノードにルートは
|
|
21167
|
+
// 来ない」はもう成り立たない(作者の復旧は取り除いて作り直す)。印が残ると、
|
|
21168
|
+
// 外してから修正版を接続するまでの窓で接続したボリュームが即座に孤児化する。
|
|
21169
|
+
// 条件は本人に限る(#257 第 3 ラウンド): 「初期化前に剥がされた要素」全部で
|
|
21170
|
+
// 落とすと、同じ rootNode の別要素(ゾンビの 2 本目・行プールの張り直し・
|
|
21171
|
+
// ロード中の DOM 移動)の切断で印が消え、以後のボリュームが孤児報告を
|
|
21172
|
+
// 受けられず永久保留へ戻る
|
|
21173
|
+
clearFailedRootNode(this._rootNode);
|
|
21174
|
+
}
|
|
19207
21175
|
this._rootNode = null;
|
|
19208
21176
|
return;
|
|
19209
21177
|
}
|
|
@@ -19217,9 +21185,14 @@ class State extends HTMLElementBase {
|
|
|
19217
21185
|
}
|
|
19218
21186
|
finally {
|
|
19219
21187
|
setStateElement(this.rootNode, null);
|
|
19220
|
-
|
|
21188
|
+
// command-token / event-token の registry は捨てない(#273)。`$on` は `_state` セッターでしか、
|
|
21189
|
+
// `command.<method>:` は値の適用でしか購読しないので、捨てると再接続の後の発火が購読者の
|
|
21190
|
+
// 居ない新しい token に届き、無言で止まる(下の stream / watch が registry を保持するのと同じ理由)。
|
|
21191
|
+
// 切断中の発火は state の解決で止まる: 要素のイベントは上の登録解除で state を引けず、
|
|
21192
|
+
// `$command` の emit は `rootNode` の無い createState を通れない。
|
|
21193
|
+
// namespace proxy の memo は破棄する(registry は残るので、再接続後の初回アクセスで
|
|
21194
|
+
// 同じ token を返す proxy が作り直される)。
|
|
19221
21195
|
clearCommandNamespace(this);
|
|
19222
|
-
clearEventTokenRegistry(this);
|
|
19223
21196
|
// stream は abort のみで registry は保持する(再接続時に同じ宣言から
|
|
19224
21197
|
// initial で再起動できる、設計書 §5-1 / §5-2)。
|
|
19225
21198
|
// namespace proxy の memo は破棄する(clearCommandNamespace と対称。
|
|
@@ -19258,6 +21231,9 @@ class State extends HTMLElementBase {
|
|
|
19258
21231
|
get watchPaths() {
|
|
19259
21232
|
return this._watchPaths;
|
|
19260
21233
|
}
|
|
21234
|
+
get scanPaths() {
|
|
21235
|
+
return this._scanPaths;
|
|
21236
|
+
}
|
|
19261
21237
|
get elementPaths() {
|
|
19262
21238
|
return this._elementPaths;
|
|
19263
21239
|
}
|
|
@@ -19337,6 +21313,10 @@ class State extends HTMLElementBase {
|
|
|
19337
21313
|
get version() {
|
|
19338
21314
|
return this._version;
|
|
19339
21315
|
}
|
|
21316
|
+
/** state の世代(キャッシュ項目の印の正本 — cache/types.ts の `generation`)。 */
|
|
21317
|
+
get stateGeneration() {
|
|
21318
|
+
return this._stateGeneration;
|
|
21319
|
+
}
|
|
19340
21320
|
get rootNode() {
|
|
19341
21321
|
if (this._rootNode === null) {
|
|
19342
21322
|
raiseError('State rootNode is not available.');
|
|
@@ -19415,6 +21395,17 @@ class State extends HTMLElementBase {
|
|
|
19415
21395
|
return this._addDependency(this._staticDependency, sourcePath, targetPath);
|
|
19416
21396
|
}
|
|
19417
21397
|
setPathInfo(path, bindingType, source = "binding") {
|
|
21398
|
+
// 再セットで作り直すための台帳(issue #258 の X7)。同じパスは複数の呼び出し元から登録
|
|
21399
|
+
// されうる(バインド・`$watch` の宣言)ので、いちど `for` で登録されたパスは `for` のまま
|
|
21400
|
+
// 保つ — `listPaths` / `elementPaths` を決めるのは下のとおり `for` だけ。保たない場合に
|
|
21401
|
+
// 何が壊れるかは `__tests__/integration.stateGenerationReset.test.ts` の
|
|
21402
|
+
// 「`for` で登録したリストパスは `$watch` の prop 登録に上書きされない」が固定する。
|
|
21403
|
+
if (source !== "internal") {
|
|
21404
|
+
const previous = this._pathRegistrations.get(path);
|
|
21405
|
+
if (typeof previous === "undefined" || previous.bindingType !== "for") {
|
|
21406
|
+
this._pathRegistrations.set(path, { bindingType, source });
|
|
21407
|
+
}
|
|
21408
|
+
}
|
|
19418
21409
|
if (bindingType === "for") {
|
|
19419
21410
|
this._listPaths.add(path);
|
|
19420
21411
|
this._elementPaths.add(path + '.' + WILDCARD);
|
|
@@ -19437,6 +21428,31 @@ class State extends HTMLElementBase {
|
|
|
19437
21428
|
}
|
|
19438
21429
|
}
|
|
19439
21430
|
}
|
|
21431
|
+
/**
|
|
21432
|
+
* 再セットで消した経路情報を、生き残ったバインドの登録から作り直す(issue #258 の X7)。
|
|
21433
|
+
*
|
|
21434
|
+
* `_pathSet.clear()` は残す。あのクリアには、`forgetGeneration` が外した静的辺を「行が
|
|
21435
|
+
* 作り直されたときに登録し直させる」自己修復が乗っている(辺が戻ることは
|
|
21436
|
+
* `__tests__/integration.stateGenerationReset.test.ts` の
|
|
21437
|
+
* 「静的な辺 nodes.* → nodes.*.total が戻る」が固定する)。消したうえで、生きているバインド
|
|
21438
|
+
* ぶんだけ `setPathInfo` をやり直す — `$recursion` のアンカーで既に同じことをしている手口を、
|
|
21439
|
+
* バインド全体へ広げたもの。
|
|
21440
|
+
*
|
|
21441
|
+
* `_generatedPaths`(これまでの世代の生成アクセサの具体パス)は張り直さない。行バインドが
|
|
21442
|
+
* 名指していても、新しい世代ではまだ実体化されていないため(recursion/generation.ts)。読みが
|
|
21443
|
+
* 実体化したときに `defineTreeAccessor` が登録し直す。
|
|
21444
|
+
*
|
|
21445
|
+
* 反復中に `setPathInfo` が台帳へ書き戻す(既存キーの上書きのみで新キーは増えない)ので、
|
|
21446
|
+
* 誤解を避けるためスナップショットを取ってから回す。
|
|
21447
|
+
*/
|
|
21448
|
+
_rebuildPathInfo() {
|
|
21449
|
+
for (const [path, registration] of Array.from(this._pathRegistrations)) {
|
|
21450
|
+
if (this._generatedPaths.has(path)) {
|
|
21451
|
+
continue;
|
|
21452
|
+
}
|
|
21453
|
+
this.setPathInfo(path, registration.bindingType, registration.source);
|
|
21454
|
+
}
|
|
21455
|
+
}
|
|
19440
21456
|
_createState(rootNode, mutability, callback) {
|
|
19441
21457
|
try {
|
|
19442
21458
|
const stateProxy = createStateProxy(rootNode, this._state, mutability);
|
|
@@ -19476,6 +21492,14 @@ class State extends HTMLElementBase {
|
|
|
19476
21492
|
}
|
|
19477
21493
|
setInitialState(state) {
|
|
19478
21494
|
if (!this._initialized) {
|
|
21495
|
+
if (this._initializeFailed) {
|
|
21496
|
+
// 初期化に失敗した要素は再武装しない(#257)。_setStatePromise は解決済みで、
|
|
21497
|
+
// ここで渡し直しても読み手が居ないため、旧挙動は無言の no-op だった。
|
|
21498
|
+
// 再武装は「落ちた宣言の残骸($on の購読・stream registry)をどう畳むか」を
|
|
21499
|
+
// 決める別の設計判断なので、ここでは唯一有効な復旧手段を伝えるに留める
|
|
21500
|
+
raiseError(`<${config.tagNames.state}> failed to initialize (the diagnostic was reported when it connected), ` +
|
|
21501
|
+
`so its state cannot be replaced. Remove this element and create a new one with the corrected state.`);
|
|
21502
|
+
}
|
|
19479
21503
|
this._resolveSetState?.(state);
|
|
19480
21504
|
return;
|
|
19481
21505
|
}
|
|
@@ -19833,6 +21857,7 @@ function getWcsManifest() {
|
|
|
19833
21857
|
STATE_ON_NAME,
|
|
19834
21858
|
STATE_STREAMS_NAME,
|
|
19835
21859
|
STATE_WATCH_NAME,
|
|
21860
|
+
STATE_SCAN_NAME,
|
|
19836
21861
|
STATE_LIST_KEYS_NAME,
|
|
19837
21862
|
STATE_RECURSION_NAME,
|
|
19838
21863
|
STATE_STREAM_STATUS_NAMESPACE_NAME,
|