@wcstack/state 2.6.0 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -7335,11 +7335,36 @@ const indexKeyedByListIndex = new WeakMap();
7335
7335
  let anyRegistered = false;
7336
7336
  const watchersByElement = new WeakMap();
7337
7337
  const watchersByIndexes = new WeakMap();
7338
+ // 祖先パス → その配下の鍵付きパス(祖先への書き込みを配下の鍵付き購読へ届ける)
7339
+ const descendantsByElement = new WeakMap();
7338
7340
  const EMPTY$1 = [];
7341
+ /** `path` の祖先を逆引きに載せる(同じ path は 1 回だけ)。祖先の無いトップレベルの path は載せない */
7342
+ function recordAncestors(stateElement, path) {
7343
+ const ledger = ledgerOf(stateElement);
7344
+ if (ledger.recorded.has(path)) {
7345
+ return;
7346
+ }
7347
+ ledger.recorded.add(path);
7348
+ const cumulativePaths = getPathInfo(path).cumulativePaths;
7349
+ if (cumulativePaths.length < 2) {
7350
+ return;
7351
+ }
7352
+ let byAncestor = descendantsByElement.get(stateElement);
7353
+ if (typeof byAncestor === "undefined") {
7354
+ descendantsByElement.set(stateElement, byAncestor = new Map());
7355
+ }
7356
+ for (let i = 0; i < cumulativePaths.length - 1; i++) {
7357
+ let descendants = byAncestor.get(cumulativePaths[i]);
7358
+ if (typeof descendants === "undefined") {
7359
+ byAncestor.set(cumulativePaths[i], descendants = new Set());
7360
+ }
7361
+ descendants.add(path);
7362
+ }
7363
+ }
7339
7364
  function ledgerOf(stateElement) {
7340
7365
  let ledger = ledgerByElement.get(stateElement);
7341
7366
  if (typeof ledger === "undefined") {
7342
- ledgerByElement.set(stateElement, ledger = { byPath: new Map(), lastValue: new Map() });
7367
+ ledgerByElement.set(stateElement, ledger = { byPath: new Map(), lastValue: new Map(), recorded: new Set() });
7343
7368
  }
7344
7369
  return ledger;
7345
7370
  }
@@ -7357,6 +7382,14 @@ function addToKey(keyMap, key, absAddress) {
7357
7382
  }
7358
7383
  set.add(absAddress);
7359
7384
  }
7385
+ /** 鍵から行を外し、空になった鍵は Map から消す(オブジェクトの鍵を行の退役後まで握らない) */
7386
+ function removeFromKey(keyMap, key, absAddress) {
7387
+ const set = keyMap.get(key);
7388
+ set.delete(absAddress);
7389
+ if (set.size === 0) {
7390
+ keyMap.delete(key);
7391
+ }
7392
+ }
7360
7393
  /** 台帳へ登録する。既に同じ (アドレス, path) が登録済みなら鍵を移すだけで、false を返す */
7361
7394
  function subscribe(ledger, path, key, absAddress) {
7362
7395
  const keyMap = keyMapOf(ledger, path);
@@ -7367,7 +7400,7 @@ function subscribe(ledger, path, key, absAddress) {
7367
7400
  if (previous.has(path)) {
7368
7401
  const oldKey = previous.get(path);
7369
7402
  if (!Object.is(oldKey, key)) {
7370
- keyMap.get(oldKey).delete(absAddress);
7403
+ removeFromKey(keyMap, oldKey, absAddress);
7371
7404
  previous.set(path, key);
7372
7405
  addToKey(keyMap, key, absAddress);
7373
7406
  }
@@ -7403,6 +7436,7 @@ function record(ledger, path, current, absAddress, isNew, entry) {
7403
7436
  function registerKeyedDependency(stateElement, path, key, absAddress, current) {
7404
7437
  anyRegistered = true;
7405
7438
  const ledger = ledgerOf(stateElement);
7439
+ recordAncestors(stateElement, path);
7406
7440
  const isNew = subscribe(ledger, path, key, absAddress);
7407
7441
  record(ledger, path, current, absAddress, isNew, { stateElement, path, absAddress, levelListIndex: null });
7408
7442
  }
@@ -7410,6 +7444,7 @@ function registerKeyedDependency(stateElement, path, key, absAddress, current) {
7410
7444
  function registerIndexKeyedDependency(stateElement, path, levelListIndex, absAddress, current) {
7411
7445
  anyRegistered = true;
7412
7446
  const ledger = ledgerOf(stateElement);
7447
+ recordAncestors(stateElement, path);
7413
7448
  const isNew = subscribe(ledger, path, levelListIndex.index, absAddress);
7414
7449
  record(ledger, path, current, absAddress, isNew, { stateElement, path, absAddress, levelListIndex });
7415
7450
  }
@@ -7421,6 +7456,7 @@ function hasKeyedDependents(stateElement, path) {
7421
7456
  function registerIndexWatcher(stateElement, path, getterPathInfo, indexes, current) {
7422
7457
  anyRegistered = true;
7423
7458
  ledgerOf(stateElement).lastValue.set(path, current);
7459
+ recordAncestors(stateElement, path);
7424
7460
  let byPath = watchersByElement.get(stateElement);
7425
7461
  if (typeof byPath === "undefined") {
7426
7462
  watchersByElement.set(stateElement, byPath = new Map());
@@ -7505,6 +7541,7 @@ function collect(set, out) {
7505
7541
  }
7506
7542
  /**
7507
7543
  * `path` への書き込み: 旧値(同値ガードが読めたとき)と新値の鍵に登録された行アドレス。
7544
+ * 控えていた最後の値が旧値と違う(旧値を読めなかった)ときは、その鍵の行も加える。
7508
7545
  * 新値を `path` の最後の値として控える。呼び出し側は hasKeyedDependents で門を通す。
7509
7546
  */
7510
7547
  function keyedDependents(stateElement, path, hasOldKey, oldKey, newKey) {
@@ -7514,13 +7551,20 @@ function keyedDependents(stateElement, path, hasOldKey, oldKey, newKey) {
7514
7551
  if (typeof ledger === "undefined" || (typeof keyMap === "undefined" && typeof watchers === "undefined")) {
7515
7552
  return EMPTY$1;
7516
7553
  }
7554
+ const hasLast = ledger.lastValue.has(path);
7555
+ const last = ledger.lastValue.get(path);
7517
7556
  ledger.lastValue.set(path, newKey);
7518
7557
  const out = [];
7519
7558
  const newDiffers = !hasOldKey || !Object.is(oldKey, newKey);
7559
+ // 行が最後に見た値。旧値を読めなかったときに真のまま残る行はここにいる
7560
+ const lastDiffers = hasLast && (!hasOldKey || !Object.is(last, oldKey)) && !Object.is(last, newKey);
7520
7561
  if (typeof keyMap !== "undefined") {
7521
7562
  if (hasOldKey) {
7522
7563
  collect(keyMap.get(oldKey), out);
7523
7564
  }
7565
+ if (lastDiffers) {
7566
+ collect(keyMap.get(last), out);
7567
+ }
7524
7568
  if (newDiffers) {
7525
7569
  collect(keyMap.get(newKey), out);
7526
7570
  }
@@ -7530,6 +7574,9 @@ function keyedDependents(stateElement, path, hasOldKey, oldKey, newKey) {
7530
7574
  if (hasOldKey) {
7531
7575
  watchedRowsAt(watcher, oldKey, out);
7532
7576
  }
7577
+ if (lastDiffers) {
7578
+ watchedRowsAt(watcher, last, out);
7579
+ }
7533
7580
  if (newDiffers) {
7534
7581
  watchedRowsAt(watcher, newKey, out);
7535
7582
  }
@@ -7537,6 +7584,57 @@ function keyedDependents(stateElement, path, hasOldKey, oldKey, newKey) {
7537
7584
  }
7538
7585
  return out;
7539
7586
  }
7587
+ function hasKeyedDescendants(stateElement, path) {
7588
+ return descendantsByElement.get(stateElement)?.has(path) === true;
7589
+ }
7590
+ /** `value` から `segments` を辿った値(途中がオブジェクトでなければ undefined) */
7591
+ function valueAt(value, segments) {
7592
+ let current = value;
7593
+ for (const segment of segments) {
7594
+ if (typeof current !== "object" || current === null) {
7595
+ return undefined;
7596
+ }
7597
+ current = current[segment];
7598
+ }
7599
+ return current;
7600
+ }
7601
+ /**
7602
+ * `path`(鍵付きパスの祖先)への書き込み: 配下の鍵付きパスごとに、旧い親と新しい親から鍵を辿り、
7603
+ * その鍵の行を返す(`keyedDependents` と同じ扱い)。残りのパスにワイルドカードがあると行ごとの鍵が
7604
+ * 辿れないので、そのパスに購読している行をすべて返す。呼び出し側は hasKeyedDescendants で門を通す。
7605
+ */
7606
+ function keyedDescendantDependents(stateElement, path, oldValue, newValue) {
7607
+ const depth = getPathInfo(path).segments.length;
7608
+ const out = [];
7609
+ for (const descendant of descendantsByElement.get(stateElement).get(path)) {
7610
+ const rest = getPathInfo(descendant).segments.slice(depth);
7611
+ if (rest.includes(WILDCARD)) {
7612
+ allDependents(stateElement, descendant, out);
7613
+ continue;
7614
+ }
7615
+ for (const address of keyedDependents(stateElement, descendant, true, valueAt(oldValue, rest), valueAt(newValue, rest))) {
7616
+ out.push(address);
7617
+ }
7618
+ }
7619
+ return out;
7620
+ }
7621
+ /** `path` に鍵付きで購読しているすべての行 */
7622
+ function allDependents(stateElement, path, out) {
7623
+ const keyMap = ledgerByElement.get(stateElement).byPath.get(path);
7624
+ if (typeof keyMap !== "undefined") {
7625
+ for (const set of keyMap.values()) {
7626
+ collect(set, out);
7627
+ }
7628
+ }
7629
+ const watchers = watchersByElement.get(stateElement)?.get(path);
7630
+ if (typeof watchers !== "undefined") {
7631
+ for (const watcher of watchers) {
7632
+ for (let index = 0; index < watcher.indexes.length; index++) {
7633
+ watchedRowsAt(watcher, index, out);
7634
+ }
7635
+ }
7636
+ }
7637
+ }
7540
7638
  function removeIndexEntry(entry) {
7541
7639
  const entries = indexKeyedByListIndex.get(entry.levelListIndex);
7542
7640
  const at = entries.indexOf(entry);
@@ -7557,7 +7655,7 @@ function dropKeyedSubscriptionsByListIndex(listIndex) {
7557
7655
  const keys = keyByPathByAddress.get(absAddress);
7558
7656
  const key = keys.get(path);
7559
7657
  keys.delete(path);
7560
- ledgerByElement.get(stateElement).byPath.get(path).get(key).delete(absAddress);
7658
+ removeFromKey(ledgerByElement.get(stateElement).byPath.get(path), key, absAddress);
7561
7659
  if (entry.levelListIndex !== null) {
7562
7660
  removeIndexEntry(entry);
7563
7661
  }
@@ -7580,7 +7678,7 @@ function rekeyIndexSubscriptions(listIndex, oldIndex, newIndex) {
7580
7678
  for (const { stateElement, path, absAddress } of entries) {
7581
7679
  const ledger = ledgerByElement.get(stateElement);
7582
7680
  const keyMap = ledger.byPath.get(path);
7583
- keyMap.get(oldIndex).delete(absAddress);
7681
+ removeFromKey(keyMap, oldIndex, absAddress);
7584
7682
  addToKey(keyMap, newIndex, absAddress);
7585
7683
  keyByPathByAddress.get(absAddress).set(path, newIndex);
7586
7684
  const last = ledger.lastValue.get(path);
@@ -11440,7 +11538,7 @@ async function buildBindings(root) {
11440
11538
  }
11441
11539
  }
11442
11540
 
11443
- var version = "2.6.0";
11541
+ var version = "2.6.1";
11444
11542
  var pkg = {
11445
11543
  version: version};
11446
11544
 
@@ -18763,16 +18861,26 @@ function recordDeclaredPrevValue(stateElement, path, absAddress, oldValue, hasOl
18763
18861
  // binding 外からの API update は新しい transaction を開始する(設計書 §4 規則 1)。
18764
18862
  // 依存 walk で enqueue される派生アドレスも同じ書き込みの因果に属する。
18765
18863
  // 鍵付き購読(`$eq` 系、dependency/keyedDependency.ts)への通知。同値ガードが読んだ旧値と
18766
- // 新値の鍵に登録された行だけを dirty 化して enqueue する。購読の無いパスは Map 参照 1 回で抜ける
18767
- function notifyKeyed(stateElement, path, hasOldValue, oldValue, value) {
18768
- if (!hasKeyedDependents(stateElement, path)) {
18864
+ // 新値の鍵に登録された行だけを dirty 化して enqueue する。購読の無いパスは Map 参照 2 回で抜ける。
18865
+ // 祖先への書き込み(`$eq("sel.id")` に対する `sel = {…}`)は配下の鍵付きパスへ知らせる。その旧値は
18866
+ // 同値ガードが読めなかったときだけ `readOld` で書き込み前に読む
18867
+ function notifyKeyed(stateElement, path, hasOldValue, oldValue, value, readOld) {
18868
+ const direct = hasKeyedDependents(stateElement, path);
18869
+ const descendants = hasKeyedDescendants(stateElement, path);
18870
+ if (!direct && !descendants) {
18769
18871
  return;
18770
18872
  }
18771
18873
  const updater = getUpdater();
18772
18874
  const context = config.enablePropagationContext ? (getCurrentPropagationContext() ?? null) : null;
18773
- for (const absAddress of keyedDependents(stateElement, path, hasOldValue, oldValue, value)) {
18875
+ const enqueue = (absAddress) => {
18774
18876
  dirtyCacheEntryByAbsoluteStateAddress(absAddress);
18775
18877
  updater.enqueueAbsoluteAddress(absAddress, context);
18878
+ };
18879
+ if (direct) {
18880
+ keyedDependents(stateElement, path, hasOldValue, oldValue, value).forEach(enqueue);
18881
+ }
18882
+ if (descendants) {
18883
+ keyedDescendantDependents(stateElement, path, hasOldValue ? oldValue : readOld(), value).forEach(enqueue);
18776
18884
  }
18777
18885
  }
18778
18886
  function notifyWrite(address, absAddress, receiver, handler, keyedMergePath, cacheable, listExpansion = "diff") {
@@ -19114,7 +19222,8 @@ function setByAddressCore(target, address, value, receiver, handler, keyedMergeP
19114
19222
  devOldValue = oldValue;
19115
19223
  devHasOldValue = true;
19116
19224
  }
19117
- notifyKeyed(stateElement, path, devHasOldValue, devOldValue, value);
19225
+ // key が undefined(listIndex の無い不正アドレス)なら読みは undefined — 書き込みが下で投げる
19226
+ notifyKeyed(stateElement, path, devHasOldValue, devOldValue, value, () => parentValue[key]);
19118
19227
  const cacheable = isCacheable(stateElement, address);
19119
19228
  const absAddress = liftAddress(stateElement, address);
19120
19229
  if (devtoolsSink !== null) {
@@ -19178,7 +19287,7 @@ function setByAddressCore(target, address, value, receiver, handler, keyedMergeP
19178
19287
  devOldValue = oldValue;
19179
19288
  devHasOldValue = true;
19180
19289
  }
19181
- notifyKeyed(stateElement, path, devHasOldValue, devOldValue, value);
19290
+ notifyKeyed(stateElement, path, devHasOldValue, devOldValue, value, () => getByAddress(target, address, receiver, handler));
19182
19291
  // --- end same-value guard ---
19183
19292
  const isSwappable = stateElement.elementPaths.has(address.pathInfo.path);
19184
19293
  const cacheable = isCacheable(stateElement, address);
@@ -20179,6 +20288,23 @@ function setLoopContext(handler, loopContext, callback) {
20179
20288
  * - 通常のプロパティアクセスもバインディングや多重ループに対応
20180
20289
  * - シンボルAPIやReflect.getで拡張性・互換性も確保
20181
20290
  */
20291
+ /**
20292
+ * 鍵付き購読の `path` が getter か getter の配下か。getter の値は `path` への書き込みを経ずに変わるので
20293
+ * 鍵付き購読では知らせられない — そのときは依存を張る普通の読み取りに戻す(正しいが、変化で全行が
20294
+ * 再評価される。鍵付きの形を使わないのと同じ)。
20295
+ */
20296
+ function derivesFromGetter(stateElement, path) {
20297
+ const getterPaths = stateElement.getterPaths;
20298
+ if (getterPaths.size === 0) {
20299
+ return false;
20300
+ }
20301
+ for (const cumulativePath of getPathInfo(path).cumulativePaths) {
20302
+ if (getterPaths.has(cumulativePath)) {
20303
+ return true;
20304
+ }
20305
+ }
20306
+ return false;
20307
+ }
20182
20308
  /** `$` + 数字だけの prop(`$1` / `$129`)。範囲外を無言で通さないための判別。 */
20183
20309
  const INDEX_PARAM_RE = /^\$\d+$/;
20184
20310
  // `$streamStatus.<name>` / `$streamError.<name>` の dotted パス判定用プレフィックス
@@ -20282,6 +20408,9 @@ function get(target, prop, receiver, handler) {
20282
20408
  return (path, key) => {
20283
20409
  // getter の外(メソッド・コールバック)ではアドレススタックが空: 比較だけ返す
20284
20410
  const lastAddress = handler.addressStackLength > 0 ? handler.lastAddressStack : null;
20411
+ if (derivesFromGetter(handler.stateElement, path)) {
20412
+ return Object.is(receiver[path], key);
20413
+ }
20285
20414
  handler.beginUntrack();
20286
20415
  let current;
20287
20416
  try {
@@ -20301,17 +20430,20 @@ function get(target, prop, receiver, handler) {
20301
20430
  // 追跡付きで行 id を読むと動的辺がリスト置換で全行に展開されるので、ここで抑止する
20302
20431
  return (path, keyPath) => {
20303
20432
  const lastAddress = handler.addressStackLength > 0 ? handler.lastAddressStack : null;
20433
+ const tracked = derivesFromGetter(handler.stateElement, path);
20434
+ let current = tracked ? receiver[path] : undefined;
20304
20435
  handler.beginUntrack();
20305
- let current;
20306
20436
  let key;
20307
20437
  try {
20308
- current = receiver[path];
20438
+ if (!tracked) {
20439
+ current = receiver[path];
20440
+ }
20309
20441
  key = receiver[keyPath];
20310
20442
  }
20311
20443
  finally {
20312
20444
  handler.endUntrack();
20313
20445
  }
20314
- if (lastAddress !== null && handler.stateElement.getterPaths.has(lastAddress.pathInfo.path)) {
20446
+ if (!tracked && lastAddress !== null && handler.stateElement.getterPaths.has(lastAddress.pathInfo.path)) {
20315
20447
  registerKeyedDependency(handler.stateElement, path, key, liftAddress(handler.stateElement, lastAddress), current);
20316
20448
  }
20317
20449
  return Object.is(current, key);
@@ -20330,6 +20462,9 @@ function get(target, prop, receiver, handler) {
20330
20462
  if (levelListIndex === null) {
20331
20463
  raiseError(`$eqIndex("${path}", ${level}): no list index at that level.`);
20332
20464
  }
20465
+ if (derivesFromGetter(handler.stateElement, path)) {
20466
+ return Object.is(receiver[path], levelListIndex.index);
20467
+ }
20333
20468
  // 最内段(getter 自身の行の段)はリスト単位の監視で O(1)、外側の段は行ごとの購読
20334
20469
  const innermost = level === lastAddress.pathInfo.wildcardCount;
20335
20470
  handler.beginUntrack();