@wcstack/state 2.0.0 → 2.1.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.d.ts CHANGED
@@ -91,7 +91,6 @@ interface IStateProxy extends IState {
91
91
  type Mutability = "readonly" | "writable";
92
92
 
93
93
  interface IStateElement {
94
- readonly name: string;
95
94
  /**
96
95
  * state のロードが完了しているか。`initializePromise` の同期版で、
97
96
  * DCC のアクセサが「今すぐ読み書きしてよいか」を判断するのに使う。
@@ -867,6 +866,13 @@ type DevtoolsEvent = {
867
866
  readonly tokenName: string;
868
867
  readonly args: readonly unknown[];
869
868
  readonly subscriberCount: number;
869
+ /**
870
+ * 発火元ツリーの state 要素(protocol v2 追補 2026-09-05・additive)。
871
+ * registry(getOrCreate*Token)経由で作られた token だけが持つ — 直接生成された
872
+ * token や旧ランタイムの payload には無い(optional)。無い emit の実測は
873
+ * ツリー別に分けられないため、消費側は全ツリーの照会へ合算で残す。
874
+ */
875
+ readonly stateElement?: IStateElement;
870
876
  } | {
871
877
  readonly type: "state:watch-error";
872
878
  /** throw 元。cur の評価(getter)とハンドラ本体では原因も直し方も違う */
@@ -883,6 +889,13 @@ type DevtoolsEvent = {
883
889
  readonly type: "state:watch-fired";
884
890
  /** `$watch` の宣言キー(ワイルドカードを含む生のパス) */
885
891
  readonly path: string;
892
+ /**
893
+ * 発火元ツリーの state 要素(protocol v2 追補 2026-09-05・additive)。
894
+ * 複数ツリーが同名の watch パスを宣言するページで実測台帳をツリー別に
895
+ * 分けるための識別。旧ランタイムの payload には無い(optional)— 無い発火は
896
+ * 消費側が全ツリーの照会へ合算で残す。値を載せない契約(§4.3.1)は不変。
897
+ */
898
+ readonly stateElement?: IStateElement;
886
899
  } | {
887
900
  readonly type: "state:path-unresolved";
888
901
  /** 書き手が書いた面。診断 code が binding / watch で変わる */
@@ -988,13 +1001,19 @@ declare function analyzeContract(manifest: IContractManifest): readonly Contract
988
1001
  declare class State extends HTMLElementBase implements IStateElement {
989
1002
  static hasConnectedCallbackPromise: boolean;
990
1003
  static getBindingsReady(rootNode: Node): Promise<void>;
1004
+ /**
1005
+ * `mount` の動的変更は未サポート(再マウントは非目標 — 設計書 §4-7)。
1006
+ * 初期化済み要素での変更は無言で捨てず warn で知らせる。初期化前の属性設定
1007
+ * (パース時・接続前の setAttribute)は正規の使い方なので黙る。
1008
+ * `name` は connectedCallback 冒頭で fail-fast 済みなので観測しない。
1009
+ */
1010
+ static get observedAttributes(): string[];
991
1011
  private __state;
992
1012
  private _hasUpdatedCallback;
993
1013
  /** enable-ssr のスナップショットから初期化された(D14: ボリュームはデータを採用する) */
994
1014
  private _hydratedFromSsr;
995
1015
  private _crossRowListPaths;
996
1016
  private _indexDependentGetterPaths;
997
- private _name;
998
1017
  private _initialized;
999
1018
  private _initializePromise;
1000
1019
  private _resolveInitialize;
@@ -1036,7 +1055,7 @@ declare class State extends HTMLElementBase implements IStateElement {
1036
1055
  constructor();
1037
1056
  private get _state();
1038
1057
  private set _state(value);
1039
- get name(): string;
1058
+ attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null): void;
1040
1059
  private _loadFromSsrElement;
1041
1060
  /** state / src / json / inner <script> / API set のソース解決(_initialize とボリュームで共用)。 */
1042
1061
  private _loadStateFromSource;
package/dist/index.esm.js CHANGED
@@ -3655,8 +3655,15 @@ class Token {
3655
3655
  // EventToken は共有 pub/sub プリミティブ Token の薄い特化(element→state 方向)。
3656
3656
  // instanceof による型判別を成立させるため独立クラスとして維持する。
3657
3657
  class EventToken extends Token {
3658
- constructor(name) {
3658
+ /**
3659
+ * 属する state 要素(devtools のツリー識別 — protocol v2 追補)。
3660
+ * registry(getOrCreateEventToken)経由の生成でのみ渡る optional 参照。
3661
+ * 寿命は registry 側の WeakMap が管理する(CommandToken と同じ位置づけ)。
3662
+ */
3663
+ _stateElement;
3664
+ constructor(name, stateElement) {
3659
3665
  super(name);
3666
+ this._stateElement = stateElement;
3660
3667
  }
3661
3668
  emit(...args) {
3662
3669
  if (devtoolsSink !== null) {
@@ -3666,6 +3673,7 @@ class EventToken extends Token {
3666
3673
  tokenName: this.name,
3667
3674
  args,
3668
3675
  subscriberCount: this.size,
3676
+ stateElement: this._stateElement,
3669
3677
  });
3670
3678
  }
3671
3679
  return super.emit(...args);
@@ -3681,7 +3689,8 @@ function getOrCreateEventToken(stateElement, name) {
3681
3689
  }
3682
3690
  let token = registry.get(name);
3683
3691
  if (typeof token === "undefined") {
3684
- token = new EventToken(name);
3692
+ // stateElement を渡すのは devtools のツリー識別(protocol v2 追補)のため
3693
+ token = new EventToken(name, stateElement);
3685
3694
  registry.set(name, token);
3686
3695
  }
3687
3696
  return token;
@@ -3806,8 +3815,16 @@ function detachEventTokenHandler(binding) {
3806
3815
  // instanceof による型判別を成立させるため独立クラスとして維持する。
3807
3816
  //
3808
3817
  class CommandToken extends Token {
3809
- constructor(name) {
3818
+ /**
3819
+ * 属する state 要素(devtools のツリー識別 — protocol v2 追補)。
3820
+ * registry(getOrCreateCommandToken)経由の生成でのみ渡る optional 参照で、
3821
+ * token の寿命は registry 側の WeakMap が state 要素に紐づけている(ここが
3822
+ * 寿命を延ばす新しい経路にはならない)。emit の payload にだけ載せる。
3823
+ */
3824
+ _stateElement;
3825
+ constructor(name, stateElement) {
3810
3826
  super(name);
3827
+ this._stateElement = stateElement;
3811
3828
  }
3812
3829
  emit(...args) {
3813
3830
  if (devtoolsSink !== null) {
@@ -3819,6 +3836,7 @@ class CommandToken extends Token {
3819
3836
  tokenName: this.name,
3820
3837
  args,
3821
3838
  subscriberCount: this.size,
3839
+ stateElement: this._stateElement,
3822
3840
  });
3823
3841
  }
3824
3842
  return super.emit(...args);
@@ -9164,7 +9182,7 @@ async function buildBindings(root) {
9164
9182
  }
9165
9183
  }
9166
9184
 
9167
- var version = "2.0.0";
9185
+ var version = "2.1.1";
9168
9186
  var pkg = {
9169
9187
  version: version};
9170
9188
 
@@ -11186,7 +11204,8 @@ function getOrCreateCommandToken(stateElement, name) {
11186
11204
  }
11187
11205
  let token = registry.get(name);
11188
11206
  if (typeof token === "undefined") {
11189
- token = new CommandToken(name);
11207
+ // stateElement を渡すのは devtools のツリー識別(protocol v2 追補)のため
11208
+ token = new CommandToken(name, stateElement);
11190
11209
  registry.set(name, token);
11191
11210
  }
11192
11211
  return token;
@@ -12774,8 +12793,10 @@ function fireOne(hit) {
12774
12793
  }
12775
12794
  // 正常発火の観測(設計書 §11 の予約イベント・配線カバレッジの実測面)。
12776
12795
  // 値は載せない。イベント生成は sink 接続時のみ(ゼロコスト契約)。
12796
+ // stateElement は発火元ツリーの識別(protocol v2 追補)— これが無いと
12797
+ // 複数ツリーが同名 watch パスを宣言するページで実測が合算される。
12777
12798
  if (devtoolsSink !== null) {
12778
- devtoolsSink({ type: "state:watch-fired", path: entry.path });
12799
+ devtoolsSink({ type: "state:watch-fired", path: entry.path, stateElement });
12779
12800
  }
12780
12801
  entry.handler.call(state, cur, prev, ...indexes);
12781
12802
  });
@@ -14654,6 +14675,34 @@ function notifyWrite(address, absAddress, receiver, handler, keyedMergePath) {
14654
14675
  // $postUpdate の手動リフレッシュは従来通り全行展開のまま)
14655
14676
  { listExpansion: "diff", keyedMergePath });
14656
14677
  }
14678
+ /**
14679
+ * 書き込み完了後のキャッシュ整合(Issue #234)。
14680
+ *
14681
+ * ワイルドカードのデータパス(リスト行)は代入値がそのまま格納値なので、
14682
+ * 代入値を dirty:false で載せて次回の読みを省く。
14683
+ *
14684
+ * アクセサペア(getterPaths に載るパス)は getter が正本であり、setter は
14685
+ * 命令的な代入に過ぎない。代入値を getter の評価結果として固定すると
14686
+ * - setter が正規化・分配した結果と読みが食い違う
14687
+ * - getter が一度も評価されず動的依存が張られない → 依存先を書いても
14688
+ * walkDependency がこのキャッシュを dirty にできず、永続的に stale になる
14689
+ * (プリミティブ代入は同値ガードの旧値読みで偶然 getter が走るが、
14690
+ * オブジェクト代入は同値ガードを素通りするため救済がない)
14691
+ * ため、キャッシュを dirty にして次回の読みで getter を再評価させる。
14692
+ */
14693
+ function commitWriteCache(stateElement, path, absAddress, value, cacheable) {
14694
+ if (!cacheable) {
14695
+ return;
14696
+ }
14697
+ if (stateElement.getterPaths.has(path)) {
14698
+ dirtyCacheEntryByAbsoluteStateAddress(absAddress);
14699
+ return;
14700
+ }
14701
+ setCacheEntryByAbsoluteStateAddress(absAddress, {
14702
+ value: value,
14703
+ dirty: false
14704
+ });
14705
+ }
14657
14706
  function _setByAddress(target, address, absAddress, value, receiver, handler, keyedMergePath) {
14658
14707
  try {
14659
14708
  if (address.pathInfo.path in target) {
@@ -14875,12 +14924,7 @@ function setByAddressCore(target, address, value, receiver, handler, keyedMergeP
14875
14924
  }
14876
14925
  finally {
14877
14926
  notifyWrite(address, absAddress, receiver, handler, keyedMergePath);
14878
- if (cacheable) {
14879
- setCacheEntryByAbsoluteStateAddress(absAddress, {
14880
- value: value,
14881
- dirty: false
14882
- });
14883
- }
14927
+ commitWriteCache(stateElement, path, absAddress, value, cacheable);
14884
14928
  // DCC bindable イベントディスパッチ(完全一致 + サブパス → 先頭セグメント、§2.1)
14885
14929
  dispatchBindableEvent(stateElement, address.pathInfo, { value });
14886
14930
  }
@@ -14927,12 +14971,7 @@ function setByAddressCore(target, address, value, receiver, handler, keyedMergeP
14927
14971
  }
14928
14972
  }
14929
14973
  finally {
14930
- if (cacheable) {
14931
- setCacheEntryByAbsoluteStateAddress(absAddress, {
14932
- value: value,
14933
- dirty: false
14934
- });
14935
- }
14974
+ commitWriteCache(stateElement, path, absAddress, value, cacheable);
14936
14975
  // DCC bindable イベントディスパッチ(完全一致 + サブパス → 先頭セグメント、§2.1)
14937
14976
  dispatchBindableEvent(stateElement, address.pathInfo, { value });
14938
14977
  }
@@ -15317,6 +15356,14 @@ function updatedCallback(target, refs, receiver, handler) {
15317
15356
  }
15318
15357
  const pathInfo = ref.absolutePathInfo.pathInfo;
15319
15358
  const pathName = pathInfo.path;
15359
+ // D20/D21: マーカーパス(`#m<id>` セグメント = マウント私有キーの内部アドレス)は
15360
+ // マウントインスタンスの私有語彙。ルートの $updatedCallback へ素通しすると、
15361
+ // 作者に解釈不能で再初期化のたびに変わる内部 id が漏れるため配送しない
15362
+ // (`#` はパス文法で書けない文字 — ツリーパスとは構造的に衝突しない)。
15363
+ // 私有キーの可視化は devtools の overlays() 経由(プロトコル v2)。
15364
+ if (pathName.indexOf("#") !== -1) {
15365
+ continue;
15366
+ }
15320
15367
  paths.add(pathName);
15321
15368
  if (pathInfo.wildcardCount > 0) {
15322
15369
  const indexes = getScopedIndexes(ref.listIndex, pathInfo.wildcardCount);
@@ -15349,6 +15396,10 @@ function updatedCallback(target, refs, receiver, handler) {
15349
15396
  if (path !== volume.mountPath && !path.startsWith(prefix)) {
15350
15397
  continue;
15351
15398
  }
15399
+ // マーカーパス(マウント私有キー)はボリューム相対配送にも漏らさない(上と同じ D20/D21)
15400
+ if (path.indexOf("#") !== -1) {
15401
+ continue;
15402
+ }
15352
15403
  const relative = path === volume.mountPath ? "" : path.slice(prefix.length);
15353
15404
  if (relative === "") {
15354
15405
  continue; // マウントポイント自身(接ぎ木そのもの)は相対で表せない
@@ -16386,6 +16437,15 @@ class State extends HTMLElementBase {
16386
16437
  static getBindingsReady(rootNode) {
16387
16438
  return getBindingsReady(rootNode);
16388
16439
  }
16440
+ /**
16441
+ * `mount` の動的変更は未サポート(再マウントは非目標 — 設計書 §4-7)。
16442
+ * 初期化済み要素での変更は無言で捨てず warn で知らせる。初期化前の属性設定
16443
+ * (パース時・接続前の setAttribute)は正規の使い方なので黙る。
16444
+ * `name` は connectedCallback 冒頭で fail-fast 済みなので観測しない。
16445
+ */
16446
+ static get observedAttributes() {
16447
+ return ["mount"];
16448
+ }
16389
16449
  __state;
16390
16450
  _hasUpdatedCallback = false;
16391
16451
  /** enable-ssr のスナップショットから初期化された(D14: ボリュームはデータを採用する) */
@@ -16396,7 +16456,6 @@ class State extends HTMLElementBase {
16396
16456
  // $1 等のインデックスを読んだ getter パス(実行時検出)。位置のみ変わった行の
16397
16457
  // 静的子展開はこの集合の subtree に限定される。追加のみ・クリアしない(安全側)。
16398
16458
  _indexDependentGetterPaths = new Set();
16399
- _name = 'default';
16400
16459
  _initialized = false;
16401
16460
  _initializePromise;
16402
16461
  _resolveInitialize = null;
@@ -16530,8 +16589,14 @@ class State extends HTMLElementBase {
16530
16589
  }
16531
16590
  this._resolveLoading?.();
16532
16591
  }
16533
- get name() {
16534
- return this._name;
16592
+ attributeChangedCallback(_name, oldValue, newValue) {
16593
+ // observedAttributes は "mount" のみ。同値 set(oldValue === newValue)は変更ではない
16594
+ if (!this._initialized || oldValue === newValue) {
16595
+ return;
16596
+ }
16597
+ console.warn(`[@wcstack/state] Changing the "mount" attribute after initialization is not supported and is ignored ` +
16598
+ `(was ${oldValue === null ? "absent" : `"${oldValue}"`}, now ${newValue === null ? "absent" : `"${newValue}"`}). ` +
16599
+ `Remove this <${config.tagNames.state}> element and create a new one with the desired mount path instead.`);
16535
16600
  }
16536
16601
  _loadFromSsrElement() {
16537
16602
  if (!this.hasAttribute('enable-ssr'))
@@ -16571,7 +16636,9 @@ class State extends HTMLElementBase {
16571
16636
  else {
16572
16637
  const script = this.querySelector('script[type="module"]');
16573
16638
  if (script) {
16574
- return await loadFromInnerScript(script, `${this._name}`);
16639
+ // sourceURL ラベル。v2 はルートに 1 ツリーなので名前次元は無く、要素の
16640
+ // タグ名(DCC 経路が host のタグ名を渡すのと同じ流儀)で特定十分
16641
+ return await loadFromInnerScript(script, config.tagNames.state);
16575
16642
  }
16576
16643
  else {
16577
16644
  const timerId = setTimeout(() => {