@wcstack/state 1.17.0 → 1.18.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/dist/index.esm.js CHANGED
@@ -63,7 +63,7 @@ function setConfig(partialConfig) {
63
63
  }
64
64
  }
65
65
 
66
- var version$1 = "1.17.0";
66
+ var version$1 = "1.18.0";
67
67
  var pkg = {
68
68
  version: version$1};
69
69
 
@@ -150,6 +150,9 @@ const STATE_COMMAND_TOKENS_NAME = "$commandTokens";
150
150
  const STATE_COMMAND_NAMESPACE_NAME = "$command";
151
151
  const STATE_EVENT_TOKENS_NAME = "$eventTokens";
152
152
  const STATE_ON_NAME = "$on";
153
+ const STATE_STREAMS_NAME = "$streams";
154
+ const STATE_STREAM_STATUS_NAMESPACE_NAME = "$streamStatus";
155
+ const STATE_STREAM_ERROR_NAMESPACE_NAME = "$streamError";
153
156
  const DCC_DEFINITION_ATTRIBUTE = "data-wc-definition";
154
157
 
155
158
  const _cache$4 = new Map();
@@ -1914,12 +1917,12 @@ function attachEventHandler(binding) {
1914
1917
  class EventToken extends Token {
1915
1918
  }
1916
1919
 
1917
- const registryByStateElement$1 = new WeakMap();
1920
+ const registryByStateElement$2 = new WeakMap();
1918
1921
  function getOrCreateEventToken(stateElement, name) {
1919
- let registry = registryByStateElement$1.get(stateElement);
1922
+ let registry = registryByStateElement$2.get(stateElement);
1920
1923
  if (typeof registry === "undefined") {
1921
1924
  registry = new Map();
1922
- registryByStateElement$1.set(stateElement, registry);
1925
+ registryByStateElement$2.set(stateElement, registry);
1923
1926
  }
1924
1927
  let token = registry.get(name);
1925
1928
  if (typeof token === "undefined") {
@@ -1929,7 +1932,7 @@ function getOrCreateEventToken(stateElement, name) {
1929
1932
  return token;
1930
1933
  }
1931
1934
  function clearEventTokenRegistry(stateElement) {
1932
- registryByStateElement$1.delete(stateElement);
1935
+ registryByStateElement$2.delete(stateElement);
1933
1936
  }
1934
1937
 
1935
1938
  /**
@@ -3669,6 +3672,37 @@ function getValue(state, binding) {
3669
3672
  }
3670
3673
  }
3671
3674
 
3675
+ // applyChange が「未 define のカスタム要素」への適用を見送った binding の台帳。
3676
+ // define されるまでの間、同じ binding に対して applyChange は(state 更新の
3677
+ // たびに)何度も呼ばれうるため、whenDefined の多重登録をここで抑止する。
3678
+ // WeakSet なので binding の寿命に追従し、恒久 define されないタグでもリークしない。
3679
+ const scheduledBindings = new WeakSet();
3680
+ /**
3681
+ * 未 define のカスタム要素に対する適用を customElements.whenDefined 後に再実行
3682
+ * する。two-way / event-token の attach、spread の deferred 展開はいずれも
3683
+ * whenDefined で再試行するのに対し、値の適用だけが片道 skip だった非対称の解消
3684
+ * (docs/state-binding-init-races.md §2)。
3685
+ *
3686
+ * 再適用は applyChangeFromBindings を通すため、define 時点の最新 state 値で
3687
+ * 適用される(skip 時点の値を保持しない)。define を待つ間に DOM から外れた
3688
+ * binding には適用しない(deferred spread と同じ規約)。
3689
+ */
3690
+ function scheduleDeferredApply(binding, tagName) {
3691
+ if (scheduledBindings.has(binding)) {
3692
+ return;
3693
+ }
3694
+ scheduledBindings.add(binding);
3695
+ customElements.whenDefined(tagName).then(() => {
3696
+ scheduledBindings.delete(binding);
3697
+ if (!binding.replaceNode.isConnected) {
3698
+ return; // define を待つ間にノードが削除された
3699
+ }
3700
+ applyChangeFromBindings([binding]);
3701
+ }).catch((error) => {
3702
+ console.error(`[@wcstack/state] deferred apply failed for <${tagName}>.`, error);
3703
+ });
3704
+ }
3705
+
3672
3706
  const applyChangeByFirstSegment = {
3673
3707
  "class": applyChangeToClass,
3674
3708
  "attr": applyChangeToAttribute,
@@ -3765,7 +3799,11 @@ function applyChange(binding, context) {
3765
3799
  const customTag = getCustomElement(binding.replaceNode);
3766
3800
  if (customTag) {
3767
3801
  if (customElements.get(customTag) === undefined) {
3768
- // cutomElement側の初期化を期待
3802
+ // 未 define のカスタム要素へは今は適用できない(accessor 未確立の要素に
3803
+ // 素の own property を書くと upgrade 後に class accessor を隠してしまう)。
3804
+ // whenDefined 後に最新 state 値で再適用する(two-way attach / deferred
3805
+ // spread と対称。docs/state-binding-init-races.md §2)。
3806
+ scheduleDeferredApply(binding, customTag);
3769
3807
  return;
3770
3808
  }
3771
3809
  }
@@ -5486,12 +5524,12 @@ function processCommandTokensDeclaration(state) {
5486
5524
  return names;
5487
5525
  }
5488
5526
 
5489
- const registryByStateElement = new WeakMap();
5527
+ const registryByStateElement$1 = new WeakMap();
5490
5528
  function getOrCreateCommandToken(stateElement, name) {
5491
- let registry = registryByStateElement.get(stateElement);
5529
+ let registry = registryByStateElement$1.get(stateElement);
5492
5530
  if (typeof registry === "undefined") {
5493
5531
  registry = new Map();
5494
- registryByStateElement.set(stateElement, registry);
5532
+ registryByStateElement$1.set(stateElement, registry);
5495
5533
  }
5496
5534
  let token = registry.get(name);
5497
5535
  if (typeof token === "undefined") {
@@ -5501,7 +5539,7 @@ function getOrCreateCommandToken(stateElement, name) {
5501
5539
  return token;
5502
5540
  }
5503
5541
  function clearCommandTokenRegistry(stateElement) {
5504
- registryByStateElement.delete(stateElement);
5542
+ registryByStateElement$1.delete(stateElement);
5505
5543
  }
5506
5544
 
5507
5545
  /**
@@ -5623,6 +5661,911 @@ function processOnDeclaration(stateElement, state, eventTokenNames) {
5623
5661
  }
5624
5662
  }
5625
5663
 
5664
+ /**
5665
+ * stream/lastNotified.ts
5666
+ *
5667
+ * 「最後に通知した観測値」台帳 — DOM binding / $updatedCallback(観測層)が
5668
+ * 最後に見た status・error(docs/state-streams-design.md §4-3)。
5669
+ *
5670
+ * 通知の same-value 判定を entry フィールドとの比較で行うと、再 set
5671
+ * (clearStreamRegistry → 新 entry 生成)を跨いだ陳腐化を検出できない
5672
+ * (error 表示中に再 set すると新 entry は error=null で生まれるため
5673
+ * null → null と誤判定して $postUpdate が落ち、DOM に旧 error が残る)。
5674
+ * そのため通知 dedup は entry の寿命ではなく stateElement の寿命で持つ
5675
+ * (ただし再 set で新宣言から消えた名前のエントリは pruneLastNotified で削除する —
5676
+ * 同名にしか dedup は要らず、放置すると台帳が単調増加するため)。
5677
+ * 未通知(初回)の基準値は宣言直後の観測初期値と同じ { idle, null }。
5678
+ *
5679
+ * さらに abortAllStreams(§5-1)は registry entry を通知なしで idle / null に
5680
+ * 直接ミューテーションするため、観測層が「台帳の値」と「idle / null」の
5681
+ * どちらを見たか確定できなくなる(binding / computed の fresh 読みは通知が
5682
+ * なくても他パスの drain で走る)。その乖離フィールドは invalidateLastNotified
5683
+ * で UNCERTAIN に無効化し、次回 updateStreamStatus の同値判定が必ず
5684
+ * 「変化あり」になるようにする(再接続ウィンドウ内の idle 描画が恒久陳腐化
5685
+ * しないための不変条件、§4-3)。
5686
+ */
5687
+ /**
5688
+ * 無通知ミューテーション後の「観測値が確定できない」印。
5689
+ * どの実値とも一致しないため、次回の通知 dedup(`!==` / `Object.is`)を強制的に解除する。
5690
+ */
5691
+ const UNCERTAIN = Symbol("wcs-stream-last-notified-uncertain");
5692
+ const lastNotifiedByStateElement = new WeakMap();
5693
+ /**
5694
+ * 最後に通知した観測値を返す。未通知なら基準値 { idle, null }。
5695
+ */
5696
+ function getLastNotified(stateElement, name) {
5697
+ return (lastNotifiedByStateElement.get(stateElement)?.get(name) ?? { status: "idle", error: null });
5698
+ }
5699
+ /**
5700
+ * 通知した観測値を記録する(updateStreamStatus が $postUpdate 発行と同時に呼ぶ)。
5701
+ */
5702
+ function setLastNotified(stateElement, name, status, error) {
5703
+ let lastMap = lastNotifiedByStateElement.get(stateElement);
5704
+ if (typeof lastMap === "undefined") {
5705
+ lastMap = new Map();
5706
+ lastNotifiedByStateElement.set(stateElement, lastMap);
5707
+ }
5708
+ lastMap.set(name, { status, error });
5709
+ }
5710
+ /**
5711
+ * 再 set(clearStreamRegistry → processStreamsDeclaration)後に呼び、新宣言に
5712
+ * 存在しない名前の台帳エントリを削除する。台帳は stateElement の寿命で生存するが
5713
+ * (§4-3 の再 set・再接続跨ぎ dedup)、それが必要なのは同名エントリのみで、
5714
+ * 旧宣言にしか無い名前は以後どの通知経路(updateStreamStatus)からも参照されない。
5715
+ * prune しないと、再 set のたびに異なる stream 名を使うステートで台帳が
5716
+ * stateElement の寿命の間単調増加する。
5717
+ * 既知の許容: prune 後に同名を再宣言した場合、dedup は基準値 { idle, null } から
5718
+ * やり直しになる(宣言削除時の binding 陳腐化が §4-4 の既知エッジである以上、
5719
+ * 再宣言は新規宣言と同じ扱いでよい)。
5720
+ */
5721
+ function pruneLastNotified(stateElement, liveNames) {
5722
+ const lastMap = lastNotifiedByStateElement.get(stateElement);
5723
+ if (typeof lastMap === "undefined") {
5724
+ return;
5725
+ }
5726
+ for (const name of lastMap.keys()) {
5727
+ if (!liveNames.has(name)) {
5728
+ lastMap.delete(name);
5729
+ }
5730
+ }
5731
+ }
5732
+ /**
5733
+ * 無通知ミューテーション(abortAllStreams の idle / null 直接書き換え)の直後に呼び、
5734
+ * 台帳のうちミューテーション後の値と一致しないフィールドを UNCERTAIN に無効化する。
5735
+ * 一致しているフィールド(観測層がどちらを見ても同じ値)は dedup を維持する
5736
+ * (例: error が null のままなら再接続時に $streamError.<name> の余計な通知は出ない)。
5737
+ */
5738
+ function invalidateLastNotified(stateElement, name) {
5739
+ const lastMap = lastNotifiedByStateElement.get(stateElement);
5740
+ if (typeof lastMap === "undefined") {
5741
+ return;
5742
+ }
5743
+ const last = lastMap.get(name);
5744
+ if (typeof last === "undefined") {
5745
+ // 未通知: 基準値 { idle, null } はミューテーション後の値と一致するため乖離しない
5746
+ return;
5747
+ }
5748
+ lastMap.set(name, {
5749
+ status: last.status === "idle" ? last.status : UNCERTAIN,
5750
+ error: Object.is(last.error, null) ? null : UNCERTAIN,
5751
+ });
5752
+ }
5753
+
5754
+ /**
5755
+ * stream/activeStateElements.ts
5756
+ *
5757
+ * 起動中(startStreams 済み・未切断)の stateElement の列挙用 Set
5758
+ * (docs/state-streams-design.md §3-2)。
5759
+ *
5760
+ * streamRegistry の WeakMap は列挙不能のため、updater の drain リスナーが
5761
+ * 「どの stateElement の entry と batch を交差させるか」を知るには
5762
+ * 列挙可能な strong Set が別途必要になる。lastNotified.ts と同じ
5763
+ * 「import 循環回避の小モジュール」パターン
5764
+ * (streamRegistry → activeStateElements ← streamRuntime の一方向依存に保つ)。
5765
+ *
5766
+ * リーク防止の不変条件(strong Set が切断済み要素の GC を妨げないための連動):
5767
+ * - add は startStreams(streamRuntime.ts)だけが行う
5768
+ * (eager 起動=connect 時、および接続中の `_state` 再 set 時の再起動)。
5769
+ * - delete は abortAllStreams / clearStreamRegistry(streamRegistry.ts)が行う。
5770
+ * disconnect(disconnectedCallback → abortAllStreams)と `_state` 再 set
5771
+ * (clearStreamRegistry → processStreamsDeclaration → 接続中なら startStreams で
5772
+ * 再 add)の両経路が必ずここを通るため、「Set に居る = 接続中かつ起動済み」が
5773
+ * 常に保たれ、切断済み stateElement への強参照は残らない。
5774
+ * 設計書 §3-2 の「未接続(disconnect 済み)の stateElement の entry は restart
5775
+ * しない」はこの不変条件で担保される。
5776
+ */
5777
+ const activeStateElements = new Set();
5778
+ /**
5779
+ * 起動中 stateElement として登録する(startStreams 専用。不変条件はモジュールヘッダ参照)。
5780
+ */
5781
+ function addActiveStateElement(stateElement) {
5782
+ activeStateElements.add(stateElement);
5783
+ }
5784
+ /**
5785
+ * 起動中 stateElement から外す(abortAllStreams / clearStreamRegistry 専用)。
5786
+ */
5787
+ function deleteActiveStateElement(stateElement) {
5788
+ activeStateElements.delete(stateElement);
5789
+ }
5790
+ /**
5791
+ * 起動中 stateElement を列挙する(drain リスナーの交差判定用)。
5792
+ */
5793
+ function getActiveStateElements() {
5794
+ return activeStateElements;
5795
+ }
5796
+
5797
+ /**
5798
+ * stream/streamRegistry.ts
5799
+ *
5800
+ * `$streams` の registry(docs/state-streams-design.md §2-1 / §5)。
5801
+ * eventTokenRegistry と対称の WeakMap registry。
5802
+ *
5803
+ * - status / error の正本は registry entry(state オブジェクト上に実プロパティは持たない)。
5804
+ * - disconnect 時は abortAllStreams(abort のみ・registry 保持)、
5805
+ * `_state` 再 set 時のみ clearStreamRegistry(abort + 全削除)。
5806
+ */
5807
+ const registryByStateElement = new WeakMap();
5808
+ /**
5809
+ * stream entry 群を置換登録する(`_state` セッターからの再構築で丸ごと差し替える)。
5810
+ */
5811
+ function setStreamEntries(stateElement, entries) {
5812
+ registryByStateElement.set(stateElement, entries);
5813
+ }
5814
+ /**
5815
+ * 登録済みの stream entry 群を返す。未登録なら空 Map を返す(registry への登録はしない)。
5816
+ */
5817
+ function getStreamEntries(stateElement) {
5818
+ return registryByStateElement.get(stateElement) ?? new Map();
5819
+ }
5820
+ /**
5821
+ * 全 stream を abort して idle に戻す(設計書 §5-1)。registry は保持する。
5822
+ *
5823
+ * disconnectedCallback(切断時)に呼ばれるため、status / error の反映は
5824
+ * proxy / $postUpdate を使わず entry への直接ミューテーションで行う
5825
+ * (切断済みで binding 更新は不要かつ rootNode が無い)。
5826
+ *
5827
+ * 無通知ミューテーションは「最後に通知した観測値」台帳(stream/lastNotified.ts)
5828
+ * と registry を乖離させるため、同時に台帳側を invalidate する。これを怠ると
5829
+ * 再接続ウィンドウ内の fresh 読み(他パスの drain での getter 再計算など)が
5830
+ * 描画した idle に対し、restart の updateStreamStatus("active") が切断前の
5831
+ * 通知値と同値判定されて skip され、DOM が恒久的に陳腐化する(設計書 §4-3)。
5832
+ */
5833
+ function abortAllStreams(stateElement) {
5834
+ // 依存駆動 restart の対象から外す(切断済み stateElement は restart しない、
5835
+ // 設計書 §3-2。add 側は startStreams — stream/activeStateElements.ts の
5836
+ // リーク防止不変条件を参照)。registry の有無に関わらず必ず外す。
5837
+ deleteActiveStateElement(stateElement);
5838
+ const entries = registryByStateElement.get(stateElement);
5839
+ if (typeof entries === "undefined") {
5840
+ return;
5841
+ }
5842
+ for (const entry of entries.values()) {
5843
+ entry.controller?.abort();
5844
+ entry.controller = null;
5845
+ entry.status = "idle";
5846
+ entry.error = null;
5847
+ invalidateLastNotified(stateElement, entry.name);
5848
+ }
5849
+ }
5850
+ /**
5851
+ * 全 stream を abort したうえで registry から削除する(`_state` 再 set 時の再配線用、設計書 §5-2)。
5852
+ */
5853
+ function clearStreamRegistry(stateElement) {
5854
+ abortAllStreams(stateElement);
5855
+ // abortAllStreams が既に delete 済みだが、「clear = 全削除でも必ず restart 対象から
5856
+ // 外れる」不変条件を将来の abortAllStreams の変更から独立に保証するため明示的に呼ぶ。
5857
+ deleteActiveStateElement(stateElement);
5858
+ registryByStateElement.delete(stateElement);
5859
+ }
5860
+
5861
+ /**
5862
+ * stream/processStreamsDeclaration.ts
5863
+ *
5864
+ * `$streams: { <name>: { args?, source, fold?, initial? } }` 宣言マップを解析し、
5865
+ * IStreamEntry を構築して streamRegistry に一括登録する
5866
+ * (docs/state-streams-design.md §1-1 / §1-2 / §1-3)。
5867
+ *
5868
+ * - バリデーション(§1-2): 違反は raiseError。
5869
+ * - 名前はフラットなプロパティ名のみ(空文字 / `.`(DELIMITER)/ `*`(WILDCARD)/ 先頭 `$` を禁止)。
5870
+ * - Object.prototype の継承名(`__proto__` / `constructor` / `toString` 等)を禁止
5871
+ * (own key でなくても `in` 判定が真になり、実体化 skip + 起動時 Reflect.set の
5872
+ * 継承 setter 化 — `__proto__` は prototype 差し替え — を引き起こすため)。
5873
+ * - getter / setter として宣言済みのパスとの衝突を禁止(getterPaths / setterPaths を検査)。
5874
+ * - `source` は関数必須。`fold` は(あれば)関数。`fold` があるのに `initial` が無ければエラー
5875
+ * (reduce は initial 必須。`initial` の有無は in 演算子で判定)。`args` は(あれば)関数。
5876
+ * - fold 省略時は latest(`(_acc, chunk) => chunk`)を注入する(§0 決定レコード)。
5877
+ * - 値プロパティ実体化(§1-3): `state[name]` が未定義なら `initial`
5878
+ * (fold 無しなら undefined)でデータプロパティとして初期化する。
5879
+ * ユーザーが同名プロパティを先に宣言していた場合は上書きしない
5880
+ * (起動時の initial リセットは streamRuntime 側の責務)。
5881
+ * - 通知 dedup 台帳の prune(§4-3): 新宣言に存在しない名前の lastNotified エントリを
5882
+ * 削除する(台帳は stateElement 寿命 — 再 set 跨ぎ dedup が必要なのは同名のみ)。
5883
+ *
5884
+ * 呼び出しは stateElement.getterPaths / setterPaths の確定後であること
5885
+ * (State の `_state` セッターが getStateInfo の反映より後に呼ぶことで保証する)。
5886
+ */
5887
+ /** fold 省略時に注入される既定 fold(latest = 最新チャンクで置換) */
5888
+ const latestFold = (_acc, chunk) => chunk;
5889
+ /** `$streams` 無し宣言の prune 用(旧宣言の全名前が残骸になる) */
5890
+ const NO_STREAM_NAMES = new Set();
5891
+ function processStreamsDeclaration(stateElement, state) {
5892
+ const declared = state[STATE_STREAMS_NAME];
5893
+ if (typeof declared === "undefined") {
5894
+ // $streams 無しの再 set でも旧宣言の名前は通知 dedup 台帳の残骸になるため prune する
5895
+ pruneLastNotified(stateElement, NO_STREAM_NAMES);
5896
+ return;
5897
+ }
5898
+ if (typeof declared !== "object" || declared === null) {
5899
+ raiseError(`${STATE_STREAMS_NAME} must be an object mapping stream names to stream definitions.`);
5900
+ }
5901
+ const entries = new Map();
5902
+ for (const [name, def] of Object.entries(declared)) {
5903
+ if (name.length === 0) {
5904
+ raiseError(`${STATE_STREAMS_NAME} entry name must be a non-empty string.`);
5905
+ }
5906
+ if (name.includes(DELIMITER)) {
5907
+ raiseError(`${STATE_STREAMS_NAME} entry "${name}" must be a flat property name ("${DELIMITER}" is not allowed).`);
5908
+ }
5909
+ if (name.includes(WILDCARD)) {
5910
+ raiseError(`${STATE_STREAMS_NAME} entry "${name}" must be a flat property name ("${WILDCARD}" is not allowed).`);
5911
+ }
5912
+ if (name.startsWith("$")) {
5913
+ raiseError(`${STATE_STREAMS_NAME} entry "${name}" must not start with "$" (reserved namespace).`);
5914
+ }
5915
+ // Object.prototype の継承名(__proto__ / constructor / toString 等)は一律拒否する。
5916
+ // own key でないのに `name in state` が真になるため実体化(§1-3)が skip され、
5917
+ // 起動時の initial リセット(Reflect.set)が継承 setter に化ける
5918
+ // (特に __proto__ は state の prototype を差し替える)ため、名前検査の防衛線で落とす(§1-2)。
5919
+ if (name in Object.prototype) {
5920
+ raiseError(`${STATE_STREAMS_NAME} entry "${name}" must not be a property name inherited from Object.prototype (e.g. "__proto__", "constructor").`);
5921
+ }
5922
+ if (stateElement.getterPaths.has(name)) {
5923
+ raiseError(`${STATE_STREAMS_NAME} entry "${name}" conflicts with a getter declared on the state.`);
5924
+ }
5925
+ if (stateElement.setterPaths.has(name)) {
5926
+ raiseError(`${STATE_STREAMS_NAME} entry "${name}" conflicts with a setter declared on the state.`);
5927
+ }
5928
+ if (typeof def !== "object" || def === null) {
5929
+ raiseError(`${STATE_STREAMS_NAME} entry "${name}" must be an object ({ args?, source, fold?, initial? }).`);
5930
+ }
5931
+ const definition = def;
5932
+ if (typeof definition.source !== "function") {
5933
+ raiseError(`${STATE_STREAMS_NAME} entry "${name}" source must be a function.`);
5934
+ }
5935
+ const hasFold = typeof definition.fold !== "undefined";
5936
+ if (hasFold && typeof definition.fold !== "function") {
5937
+ raiseError(`${STATE_STREAMS_NAME} entry "${name}" fold must be a function.`);
5938
+ }
5939
+ if (hasFold && !("initial" in definition)) {
5940
+ raiseError(`${STATE_STREAMS_NAME} entry "${name}" requires "initial" when fold is specified (reduce needs a seed value).`);
5941
+ }
5942
+ const hasArgs = typeof definition.args !== "undefined";
5943
+ if (hasArgs && typeof definition.args !== "function") {
5944
+ raiseError(`${STATE_STREAMS_NAME} entry "${name}" args must be a function.`);
5945
+ }
5946
+ const entry = {
5947
+ name,
5948
+ definition: {
5949
+ args: definition.args ?? null,
5950
+ source: definition.source,
5951
+ fold: definition.fold ?? latestFold,
5952
+ initial: definition.initial,
5953
+ },
5954
+ status: "idle",
5955
+ error: null,
5956
+ controller: null,
5957
+ depAddresses: new Set(),
5958
+ };
5959
+ // 値プロパティ実体化(§1-3): ユーザーが同名プロパティを先に宣言していたら上書きしない
5960
+ if (!(name in state)) {
5961
+ state[name] = entry.definition.initial;
5962
+ }
5963
+ entries.set(name, entry);
5964
+ }
5965
+ setStreamEntries(stateElement, entries);
5966
+ // 新宣言に存在しない名前の通知 dedup 台帳エントリを prune する
5967
+ // (同名は保持 = §4-3 の再 set 跨ぎ dedup 契約を維持。stream/lastNotified.ts 参照)
5968
+ pruneLastNotified(stateElement, new Set(entries.keys()));
5969
+ }
5970
+
5971
+ /**
5972
+ * stream/streamNamespace.ts
5973
+ *
5974
+ * `$streamStatus` / `$streamError` の read-only namespace proxy
5975
+ * (docs/state-streams-design.md §4-1 / §4-2)。commandNamespace と対称。
5976
+ *
5977
+ * - state element 単位で memo 化し、同一 stateElement なら同じ proxy が返る。
5978
+ * - 宣言された stream 名(`$streams` に列挙されたもの)のみ registry entry の
5979
+ * status / error を返す。宣言外の名前・Symbol キーは undefined
5980
+ * (`then` / `constructor` 等を内部機構が触っても throw しない寛容規約、
5981
+ * $command と同じ)。
5982
+ * - 値は memo しない: proxy は getStreamEntries を毎回読む thin gateway
5983
+ * (status / error は runtime が随時書き換えるため。registry entry が正本、§2-1)。
5984
+ * - set / deleteProperty は raiseError。setByAddress の親走査が namespace proxy に
5985
+ * 到達したときの Reflect.set もここで落ちる(書き込み防御 S11 の終端)。
5986
+ */
5987
+ const statusNamespaceByStateElement = new WeakMap();
5988
+ const errorNamespaceByStateElement = new WeakMap();
5989
+ function createStreamNamespaceProxy(stateElement, namespaceName, pick) {
5990
+ return new Proxy(Object.create(null), {
5991
+ get(_target, prop) {
5992
+ if (typeof prop !== "string") {
5993
+ return undefined;
5994
+ }
5995
+ const entry = getStreamEntries(stateElement).get(prop);
5996
+ if (typeof entry === "undefined") {
5997
+ return undefined;
5998
+ }
5999
+ return pick(entry);
6000
+ },
6001
+ has(_target, prop) {
6002
+ return typeof prop === "string" && getStreamEntries(stateElement).has(prop);
6003
+ },
6004
+ ownKeys() {
6005
+ return Array.from(getStreamEntries(stateElement).keys());
6006
+ },
6007
+ getOwnPropertyDescriptor(_target, prop) {
6008
+ if (typeof prop !== "string") {
6009
+ return undefined;
6010
+ }
6011
+ const entry = getStreamEntries(stateElement).get(prop);
6012
+ if (typeof entry === "undefined") {
6013
+ return undefined;
6014
+ }
6015
+ return {
6016
+ configurable: true,
6017
+ enumerable: true,
6018
+ value: pick(entry),
6019
+ };
6020
+ },
6021
+ set() {
6022
+ raiseError(`${namespaceName} namespace is read-only; assigning to it is not allowed.`);
6023
+ },
6024
+ deleteProperty() {
6025
+ raiseError(`${namespaceName} namespace is read-only; deleting from it is not allowed.`);
6026
+ },
6027
+ });
6028
+ }
6029
+ function getStreamStatusNamespace(stateElement) {
6030
+ const cached = statusNamespaceByStateElement.get(stateElement);
6031
+ if (typeof cached !== "undefined") {
6032
+ return cached;
6033
+ }
6034
+ const proxy = createStreamNamespaceProxy(stateElement, STATE_STREAM_STATUS_NAMESPACE_NAME, (entry) => entry.status);
6035
+ statusNamespaceByStateElement.set(stateElement, proxy);
6036
+ return proxy;
6037
+ }
6038
+ function getStreamErrorNamespace(stateElement) {
6039
+ const cached = errorNamespaceByStateElement.get(stateElement);
6040
+ if (typeof cached !== "undefined") {
6041
+ return cached;
6042
+ }
6043
+ const proxy = createStreamNamespaceProxy(stateElement, STATE_STREAM_ERROR_NAMESPACE_NAME, (entry) => entry.error);
6044
+ errorNamespaceByStateElement.set(stateElement, proxy);
6045
+ return proxy;
6046
+ }
6047
+ /**
6048
+ * 両 namespace proxy の memo を破棄する(clearCommandNamespace と対称)。
6049
+ * disconnectedCallback と `_state` 再 set 時に呼ばれる。
6050
+ */
6051
+ function clearStreamNamespace(stateElement) {
6052
+ statusNamespaceByStateElement.delete(stateElement);
6053
+ errorNamespaceByStateElement.delete(stateElement);
6054
+ }
6055
+
6056
+ const updateBatchListeners = new Set();
6057
+ /**
6058
+ * drain 終了リスナーを登録する。
6059
+ */
6060
+ function registerUpdateBatchListener(listener) {
6061
+ updateBatchListeners.add(listener);
6062
+ }
6063
+ /**
6064
+ * 全リスナーに drain のバッチを通知する。
6065
+ * リスナーの throw は握りつぶさない(内部バグの隠蔽防止)。
6066
+ * stream 側リスナーが entry ごとに自前で try/catch する契約(設計書 §3-2)。
6067
+ */
6068
+ function notifyUpdateBatchListeners(batch) {
6069
+ for (const listener of updateBatchListeners) {
6070
+ listener(batch);
6071
+ }
6072
+ }
6073
+ class Updater {
6074
+ _queueAbsoluteAddresses = [];
6075
+ constructor() {
6076
+ }
6077
+ enqueueAbsoluteAddress(absoluteAddress) {
6078
+ const requireStartProcess = this._queueAbsoluteAddresses.length === 0;
6079
+ this._queueAbsoluteAddresses.push(absoluteAddress);
6080
+ if (requireStartProcess) {
6081
+ queueMicrotask(() => {
6082
+ const absoluteAddresses = this._queueAbsoluteAddresses;
6083
+ this._queueAbsoluteAddresses = [];
6084
+ this._applyChange(absoluteAddresses);
6085
+ });
6086
+ }
6087
+ }
6088
+ // テスト用に公開
6089
+ testApplyChange(absoluteAddresses) {
6090
+ this._applyChange(absoluteAddresses);
6091
+ }
6092
+ _applyChange(absoluteAddresses) {
6093
+ // Note: AbsoluteStateAddress はキャッシュされているため、
6094
+ // 同一の (stateName, address) は同じインスタンスとなり、
6095
+ // Set による重複排除が正しく機能する
6096
+ const absoluteAddressSet = new Set(absoluteAddresses);
6097
+ const processBindings = [];
6098
+ for (const absoluteAddress of absoluteAddressSet) {
6099
+ const bindings = getBindingSetByAbsoluteStateAddress(absoluteAddress);
6100
+ for (const binding of bindings) {
6101
+ if (binding.replaceNode.isConnected === false) {
6102
+ // 切断されているバインディングは無視
6103
+ continue;
6104
+ }
6105
+ processBindings.push(binding);
6106
+ }
6107
+ }
6108
+ applyChangeFromBindings(processBindings);
6109
+ // drain 終了フック: binding 適用後に dedup 済みバッチを通知する(設計書 §3-2)。
6110
+ // testApplyChange も同じ _applyChange を通るため、テストから同期に駆動できる。
6111
+ notifyUpdateBatchListeners(absoluteAddressSet);
6112
+ }
6113
+ }
6114
+ const updater = new Updater();
6115
+ function getUpdater() {
6116
+ return updater;
6117
+ }
6118
+
6119
+ /**
6120
+ * stream/argsTrace.ts
6121
+ *
6122
+ * `$streams` の args トレース(依存捕捉、docs/state-streams-design.md §3-1)。
6123
+ *
6124
+ * - モジュールスコープの collector を立てて readonly proxy 上で args を評価し、
6125
+ * getByAddress を通った読みを絶対アドレス(IAbsoluteStateAddress)として捕捉する。
6126
+ * AbsolutePathInfo / AbsoluteStateAddress は両方キャッシュ済みのため、捕捉した
6127
+ * アドレスは drain バッチと Set.has のインスタンス同一性で O(1) 照合できる(§2-1)。
6128
+ * - collectStreamDependency は getByAddress のホットパスから毎読み呼ばれるため、
6129
+ * collector === null なら即 return し、それ以外の計算を一切しない。
6130
+ * - 起動・restart のたびに traceArgs が呼ばれ、成功時は entry.depAddresses を
6131
+ * 丸ごと置換する(per-run の動的再捕捉)。失敗時は前回成功 run の検証済み
6132
+ * 捕捉を保持する(§2-2 の「error からも依存変化で restart」を保つ)。
6133
+ * - lastNotified.ts と同じく import 循環回避のための小モジュール
6134
+ * (getByAddress → argsTrace ← streamRuntime の一方向依存に保つ)。
6135
+ */
6136
+ /** トレース中のみ非 null。getByAddress を通った読みの絶対アドレスが溜まる。 */
6137
+ let collector = null;
6138
+ /**
6139
+ * getByAddress の入口(checkDependency 直後)から毎読み呼ばれるフック。
6140
+ * トレース外(collector === null)では何もしない。
6141
+ */
6142
+ function collectStreamDependency(stateElement, address) {
6143
+ if (collector === null) {
6144
+ return;
6145
+ }
6146
+ const absolutePathInfo = getAbsolutePathInfo(stateElement, address.pathInfo);
6147
+ collector.add(createAbsoluteStateAddress(absolutePathInfo, address.listIndex));
6148
+ }
6149
+ /**
6150
+ * args を readonly proxy で同期評価し、読まれたパスを entry.depAddresses に
6151
+ * 丸ごと置換で再捕捉する(§3-1)。評価値(source の第 1 引数になる)を返す。
6152
+ *
6153
+ * - args === null(宣言で省略)なら depAddresses を clear して undefined
6154
+ * (依存なし = 起動後 restart しない)。
6155
+ * - 検査(違反は raiseError):
6156
+ * (a) 評価値が Promise(同期契約違反)
6157
+ * (b) 自己依存 — `<name>` / `$streamStatus.<name>` / `$streamError.<name>` の読み
6158
+ * (restart の自己書き込みで再発火する無限ループ、S8)
6159
+ * (c) wildcard を含むパスの読み(`$getAll` 等も同様。第 1 段スコープ外)
6160
+ * - 失敗時(args のユーザー例外・検査違反)は今回の捕捉(captured)を採用せず
6161
+ * 伝播し、entry.depAddresses には**前回成功 run の検証済み捕捉を保持する**。
6162
+ * これにより drain リスナーが throw を error 経路に正規化したあとも、依存の
6163
+ * 書き込みで再試行できる(§2-2「done / error からも依存変化で restart」——
6164
+ * 一時的な args throw で stream が恒久固着しない)。ループ安全性:
6165
+ * 保持されるのは前回**成功** run の捕捉のみ(自己依存・wildcard 検査済み)で
6166
+ * 自分の `<name>` / `$streamStatus.<name>` / `$streamError.<name>` を含み得ず、
6167
+ * traceArgs throw 時の startStream は initial リセットに到達しないため、
6168
+ * error 正規化の書き込みが保持 deps に再 hit することはない。再試行は依存
6169
+ * 書き込み 1 回につき高々 1 回で有界。未検査の captured を採用しないことが
6170
+ * ループ防止の要件であり、前回検証済み捕捉の保持はそれを侵さない。
6171
+ * - collector は finally で必ず復元する(例外・再入安全。ネスト評価は想定しないが
6172
+ * 防御的に「前の collector を復元」の形にしておく — コストは同等)。
6173
+ */
6174
+ function traceArgs(stateElement, entry) {
6175
+ const argsFn = entry.definition.args;
6176
+ if (argsFn === null) {
6177
+ entry.depAddresses.clear();
6178
+ return undefined;
6179
+ }
6180
+ const previousCollector = collector;
6181
+ const captured = new Set();
6182
+ collector = captured;
6183
+ let argsValue = undefined;
6184
+ try {
6185
+ stateElement.createState("readonly", (state) => {
6186
+ argsValue = argsFn(state);
6187
+ });
6188
+ }
6189
+ finally {
6190
+ // args のユーザー例外時は captured を採用せずそのまま伝播する
6191
+ // (entry.depAddresses は前回成功 run の検証済み捕捉を保持)
6192
+ collector = previousCollector;
6193
+ }
6194
+ if (argsValue instanceof Promise) {
6195
+ raiseError(`${STATE_STREAMS_NAME} entry "${entry.name}" args must be synchronous (it returned a Promise).`);
6196
+ }
6197
+ const selfStatusPath = `${STATE_STREAM_STATUS_NAMESPACE_NAME}${DELIMITER}${entry.name}`;
6198
+ const selfErrorPath = `${STATE_STREAM_ERROR_NAMESPACE_NAME}${DELIMITER}${entry.name}`;
6199
+ for (const dep of captured) {
6200
+ const pathInfo = dep.absolutePathInfo.pathInfo;
6201
+ if (dep.absolutePathInfo.stateElement === stateElement &&
6202
+ (pathInfo.path === entry.name || pathInfo.path === selfStatusPath || pathInfo.path === selfErrorPath)) {
6203
+ raiseError(`${STATE_STREAMS_NAME} entry "${entry.name}" args must not read the stream itself ("${pathInfo.path}"): a self-dependency would restart the stream on its own writes (infinite loop).`);
6204
+ }
6205
+ if (pathInfo.wildcardCount > 0) {
6206
+ raiseError(`${STATE_STREAMS_NAME} entry "${entry.name}" args must not read wildcard paths ("${pathInfo.path}"): wildcard dependencies are out of scope.`);
6207
+ }
6208
+ }
6209
+ entry.depAddresses = captured;
6210
+ return argsValue;
6211
+ }
6212
+
6213
+ /**
6214
+ * stream/consumeSource.ts
6215
+ *
6216
+ * `$streams` のチャンク消費ループ(docs/state-streams-design.md §3-3)。
6217
+ * packages/signals/src/streamResource.ts の consume / iterate /
6218
+ * readableToAsyncIterable の移植(パッケージ間依存は持たない自己完結原則)。
6219
+ *
6220
+ * 唯一の構造差分は状態書き込みの IConsumeSink への委譲:
6221
+ * value.set(fold(value.peek(), chunk)) → sink.fold(chunk)
6222
+ * status.set("done") → sink.done()
6223
+ * error.set(e) + status.set("error") → sink.fail(e)
6224
+ *
6225
+ * sink.fold() が throw した場合(fold throw)もループ内の throw として
6226
+ * 既存の catch に流れ、signal.aborted なら return、でなければ sink.fail(e)。
6227
+ * consumeSource 自体は fold throw と source throw を区別しない
6228
+ * (producer の掃除 = controller.abort() は呼び出し側 runtime が fail 内で行う)。
6229
+ * consumeSource は reject しない(全経路 catch 済み)。
6230
+ *
6231
+ * ---------------------------------------------------------------------------
6232
+ * 以下、移植元モジュールヘッダの契約(原文英語のまま維持):
6233
+ *
6234
+ * CONTRACT (cooperative cancellation — STRONG REQUIREMENT): the `source` MUST honor
6235
+ * the `AbortSignal` it is given. Honoring it is what drives switchMap restart/dispose;
6236
+ * a source that ignores it cannot be reliably cancelled.
6237
+ *
6238
+ * Rescue levels on abort:
6239
+ * - ReadableStream: FULLY rescued. A parked read() is force-unwound via
6240
+ * reader.cancel(), which both releases the underlying source and settles the
6241
+ * pending read() so the loop unwinds.
6242
+ * - AsyncIterable / async generator: PARTIALLY rescued. On abort we call
6243
+ * iterator.return() to trigger the generator's finally/cleanup. But a parked
6244
+ * `await` (the producer stalling before its next yield while IGNORING `signal`)
6245
+ * cannot be force-unwound from outside — return() only takes effect when the
6246
+ * generator next resumes. So a source that parks forever and never observes
6247
+ * `signal` still leaks its consume task. Honor `signal` to bound this.
6248
+ * The `if (signal.aborted) return` check only runs after a chunk arrives, not while
6249
+ * parked — it drops stale chunks but is not, by itself, a cancellation mechanism.
6250
+ */
6251
+ async function consumeSource(source, args, signal, sink) {
6252
+ // Obtain the iterator EXPLICITLY (not via `for await`'s implicit one) so abort can
6253
+ // call `iterator.return()` to trigger an AsyncIterable / async generator's
6254
+ // `finally`/cleanup. A `for await` only calls `.return()` when the loop itself exits;
6255
+ // if the producer is PARKED (awaiting before the next yield while ignoring `signal`),
6256
+ // the loop never advances, so the implicit `.return()` never runs and the task leaks
6257
+ // past restart/dispose. Calling `.return()` on abort is the PARTIAL rescue: the
6258
+ // parked `await` cannot be force-unwound from outside, but once the generator resumes
6259
+ // (its next tick), `.return()` makes it run its `finally` and stop — recovering the
6260
+ // common "generator wakes up after abort" case. The ReadableStream path is fully
6261
+ // rescued via `reader.cancel()` (see `readableToAsyncIterable`).
6262
+ let iterator = null;
6263
+ // Guard against returning the SAME iterator twice. `onAbort` is reachable two ways:
6264
+ // the abort listener, and the explicit call below when abort raced the
6265
+ // `await source(...)`. The guard keys on the iterator instance (not a plain "ran"
6266
+ // flag): the listener firing with iterator still null must NOT consume the single
6267
+ // real cleanup that the explicit call performs once the iterator exists. So we only
6268
+ // mark an iterator returned once we have actually called `.return()` on it.
6269
+ let returned = null;
6270
+ const onAbort = () => {
6271
+ if (!iterator || iterator === returned) {
6272
+ return; // nothing to release yet, or already released this iterator
6273
+ }
6274
+ returned = iterator;
6275
+ // Fire the iterator's cleanup. Swallow any throw/rejection from `.return()` — we
6276
+ // are tearing down; a producer that rejects on return must not surface here.
6277
+ try {
6278
+ void iterator.return?.()?.then?.(undefined, () => { });
6279
+ }
6280
+ catch {
6281
+ // `.return()` threw synchronously while tearing down — ignore.
6282
+ }
6283
+ };
6284
+ signal.addEventListener("abort", onAbort, { once: true });
6285
+ try {
6286
+ const produced = await source(args, signal);
6287
+ iterator = iterate(produced, signal)[Symbol.asyncIterator]();
6288
+ if (signal.aborted) {
6289
+ // Aborted while awaiting the source: the abort listener already ran (iterator
6290
+ // was still null then), so explicitly release the just-produced iterator now —
6291
+ // this fires a generator's finally / a ReadableStream's cancel for the
6292
+ // resource we created but will never iterate.
6293
+ onAbort();
6294
+ return;
6295
+ }
6296
+ for (;;) {
6297
+ const result = await iterator.next();
6298
+ if (result.done) {
6299
+ break;
6300
+ }
6301
+ if (signal.aborted) {
6302
+ return; // stale chunk from a superseded/disposed run — drop it
6303
+ }
6304
+ sink.fold(result.value);
6305
+ }
6306
+ if (signal.aborted) {
6307
+ return; // stream ended but this run was aborted — don't mark done
6308
+ }
6309
+ sink.done();
6310
+ }
6311
+ catch (e) {
6312
+ if (signal.aborted) {
6313
+ return; // an abort that surfaced as a throw is not an error
6314
+ }
6315
+ sink.fail(e); // keep the last folded value (do not reset)
6316
+ }
6317
+ finally {
6318
+ signal.removeEventListener("abort", onAbort);
6319
+ }
6320
+ }
6321
+ function iterate(produced, signal) {
6322
+ // Optional chaining: a null/undefined source return value must fall through to the
6323
+ // explicit TypeError below (symmetric with the `?.` on the getReader probe), not
6324
+ // throw an opaque "Cannot read properties of null" from this property access.
6325
+ if (typeof produced?.[Symbol.asyncIterator] === "function") {
6326
+ return produced;
6327
+ }
6328
+ // Not async-iterable: must be a ReadableStream (read via getReader). Validate so
6329
+ // a wrong source value yields a clear error instead of an opaque "getReader is
6330
+ // not a function" from inside the generator.
6331
+ if (typeof produced?.getReader !== "function") {
6332
+ throw new TypeError("[@wcstack/state] $streams: source must return an AsyncIterable or a ReadableStream (got neither).");
6333
+ }
6334
+ return readableToAsyncIterable(produced, signal);
6335
+ }
6336
+ async function* readableToAsyncIterable(stream, signal) {
6337
+ const reader = stream.getReader();
6338
+ // A ReadableStream read() does NOT observe an AbortSignal on its own. Without
6339
+ // this, a switchMap restart / dispose leaves the previous reader parked in a
6340
+ // pending read() forever, leaking the underlying source. Cancelling on abort
6341
+ // both releases the source AND settles the pending read() so the for-await
6342
+ // unwinds and the finally below can release the lock. Abort is the only
6343
+ // early-exit path for this generator (the consumer never calls .return()
6344
+ // without aborting), so this is the sole place a non-drained stream is cancelled.
6345
+ const onAbort = () => {
6346
+ void reader.cancel().catch(() => { }); // tearing down; swallow a rejected cancel
6347
+ };
6348
+ signal.addEventListener("abort", onAbort, { once: true });
6349
+ try {
6350
+ for (;;) {
6351
+ const { done, value } = await reader.read();
6352
+ if (done) {
6353
+ return;
6354
+ }
6355
+ yield value;
6356
+ }
6357
+ }
6358
+ finally {
6359
+ signal.removeEventListener("abort", onAbort);
6360
+ reader.releaseLock();
6361
+ }
6362
+ }
6363
+
6364
+ /**
6365
+ * stream/streamRuntime.ts
6366
+ *
6367
+ * `$streams` の起動・チャンク反映・status 遷移(docs/state-streams-design.md
6368
+ * §2-2 / §3-3 / §4-3)。
6369
+ *
6370
+ * スコープ:
6371
+ * - eager 起動(startStreams)と start = restart の共通手順(startStream)。
6372
+ * - args は traceArgs(stream/argsTrace.ts)で readonly proxy 評価と同時に依存を
6373
+ * per-run 再捕捉する(§3-1)。
6374
+ * - 依存駆動 restart(§3-2): モジュール初期化時に updater の drain 終了リスナーを
6375
+ * 1 つ登録し(restartStreamsOnUpdateBatch)、起動中 stateElement
6376
+ * (stream/activeStateElements.ts — startStreams で add・abortAllStreams /
6377
+ * clearStreamRegistry で delete)の各 entry について depAddresses と batch を
6378
+ * 交差させ、hit した entry を restart する。
6379
+ *
6380
+ * 切断後の後始末について(不変条件):
6381
+ * - disconnect(abortAllStreams)は registry entry を直接ミューテーションして
6382
+ * idle に戻す($postUpdate は呼ばない — 切断済みで binding 更新は不要かつ
6383
+ * rootNode が無い)。
6384
+ * - abort 済み run の sink コールバック(fold / done / fail)は consumeSource の
6385
+ * stale-drop(全経路の signal.aborted チェック)が createState 到達前に
6386
+ * 落とすため、runtime 側に切断後ガードは不要。
6387
+ * 「runtime が createState を呼ぶのは自分の controller が生きている間だけ」が
6388
+ * この 2 つの組み合わせで常に保たれる。
6389
+ */
6390
+ /**
6391
+ * 登録済みの全 stream を起動する(eager 起動、設計書 §2-3)。
6392
+ * State.connectedCallback($connectedCallback 完了後)と接続中の `_state` 再 set
6393
+ * から呼ばれる想定。
6394
+ *
6395
+ * 同時に依存駆動 restart(§3-2)の対象として activeStateElements に登録する
6396
+ * (delete 側は abortAllStreams / clearStreamRegistry —
6397
+ * stream/activeStateElements.ts のリーク防止不変条件を参照)。
6398
+ * eager 起動の throw(args のユーザー例外等)はここでは正規化せず loud fail のまま
6399
+ * (既存の $connectedCallback と同じ扱い。正規化は drain リスナー側の restart のみ)。
6400
+ */
6401
+ function startStreams(stateElement) {
6402
+ const entries = getStreamEntries(stateElement);
6403
+ if (entries.size === 0) {
6404
+ return;
6405
+ }
6406
+ addActiveStateElement(stateElement);
6407
+ for (const entry of entries.values()) {
6408
+ startStream(stateElement, entry);
6409
+ }
6410
+ }
6411
+ /**
6412
+ * stream を起動する。start = restart の共通手順(設計書 §2-2):
6413
+ *
6414
+ * 1. 旧 run を abort(restart 時)→ 新 AbortController
6415
+ * 2. traceArgs で args を readonly proxy 評価し依存を丸ごと再捕捉
6416
+ * (Promise / 自己依存 / wildcard 読みは raiseError、§3-1)
6417
+ * 3. 値を initial にリセット(起動 = 最初の run も restart と同一セマンティクス、§1-3)
6418
+ * 4. status="active"・error=null を反映
6419
+ * 5. consumeSource で消費開始
6420
+ */
6421
+ function startStream(stateElement, entry) {
6422
+ entry.controller?.abort();
6423
+ const controller = new AbortController();
6424
+ entry.controller = controller;
6425
+ // args 評価 + 依存の per-run 再捕捉(args === null なら depAddresses を clear して
6426
+ // undefined。Promise / 自己依存 / wildcard 読みは raiseError、§3-1)
6427
+ const argsValue = traceArgs(stateElement, entry);
6428
+ // 値リセット: setByAddress を通すことで updater coalesce・sameValueGuard・
6429
+ // walkDependency(stream 値に依存する computed の dirty 化)がすべて乗る(§3-3)
6430
+ stateElement.createState("writable", (state) => {
6431
+ state[entry.name] = entry.definition.initial;
6432
+ });
6433
+ updateStreamStatus(stateElement, entry, "active", null);
6434
+ const definition = entry.definition;
6435
+ const sink = {
6436
+ fold(chunk) {
6437
+ // fold の throw はそのまま伝播させる(consumeSource が fail 経路に回す)
6438
+ stateElement.createState("writable", (state) => {
6439
+ state[entry.name] = definition.fold(state[entry.name], chunk);
6440
+ });
6441
+ },
6442
+ done() {
6443
+ updateStreamStatus(stateElement, entry, "done", null);
6444
+ },
6445
+ fail(error) {
6446
+ // 値は直前の fold 結果を保持(リセットしない)
6447
+ updateStreamStatus(stateElement, entry, "error", error);
6448
+ // fold-throw 時の producer 掃除(iterator.return() / reader.cancel() を発火)。
6449
+ // source-throw 時は producer が既に終了しているので abort は無害(§3-3)。
6450
+ controller.abort();
6451
+ },
6452
+ };
6453
+ void consumeSource(definition.source, argsValue, controller.signal, sink);
6454
+ }
6455
+ /**
6456
+ * status / error の反映ヘルパ(設計書 §4-3)。
6457
+ *
6458
+ * - registry entry が正本。常に最新値へ書き換える。
6459
+ * - 「最後に通知した観測値」(stream/lastNotified.ts — 再 set・再接続を跨いで
6460
+ * stateElement の寿命で生存する台帳)から変化した項目に対応する名前空間パス
6461
+ * (`$streamStatus.<name>` / `$streamError.<name>`)だけを writable proxy の
6462
+ * $postUpdate で通知する(updater enqueue + walkDependency)。
6463
+ * - 両方不変なら通知しない(名前空間パスは setByAddress を通らないため
6464
+ * sameValueGuard が効かず、同等の same-value 判定を runtime 側が持つ)。
6465
+ * abortAllStreams の無通知ミューテーションで台帳が invalidate されている場合は
6466
+ * 同値扱いにならず必ず通知される(再接続ウィンドウ内の fresh 読みが描画した
6467
+ * idle の恒久陳腐化を防ぐ、§4-3)。
6468
+ */
6469
+ function updateStreamStatus(stateElement, entry, status, error) {
6470
+ entry.status = status;
6471
+ entry.error = error;
6472
+ const last = getLastNotified(stateElement, entry.name);
6473
+ const statusChanged = last.status !== status;
6474
+ const errorChanged = !Object.is(last.error, error);
6475
+ if (!statusChanged && !errorChanged) {
6476
+ return;
6477
+ }
6478
+ setLastNotified(stateElement, entry.name, status, error);
6479
+ stateElement.createState("writable", (state) => {
6480
+ if (statusChanged) {
6481
+ state.$postUpdate(`${STATE_STREAM_STATUS_NAMESPACE_NAME}${DELIMITER}${entry.name}`);
6482
+ }
6483
+ if (errorChanged) {
6484
+ state.$postUpdate(`${STATE_STREAM_ERROR_NAMESPACE_NAME}${DELIMITER}${entry.name}`);
6485
+ }
6486
+ });
6487
+ }
6488
+ /**
6489
+ * 依存駆動 restart の drain リスナー(設計書 §3-2)。
6490
+ * モジュール初期化時に registerUpdateBatchListener で 1 つだけ登録される。
6491
+ *
6492
+ * - 起動中の各 stateElement の各 entry について、depAddresses と batch の交差を
6493
+ * Set.has のインスタンス同一性で判定する(小さい方 = depAddresses を回して
6494
+ * batch.has(dep)。AbsoluteStateAddress はキャッシュにより同一 (stateName, path,
6495
+ * listIndex) が同一インスタンス、§2-1)。args なし(depAddresses 空)の entry は
6496
+ * 自然にスキップされる。
6497
+ * - status は問わず restart する(done / error からも依存の叩き直しで再試行、§2-2)。
6498
+ * - hit は収集してから一括で restart する(イテレーション中の registry 変更を避ける。
6499
+ * entry ごとに最初の hit で break するため「1 drain につき 1 entry 最大 1 restart」
6500
+ * もここで自然に成立する — 同一 tick 内の複数依存書き込みは 1 restart に畳まれる)。
6501
+ * - hits の実行時にも active + entry identity を再チェックする: 先行 restart の
6502
+ * source / args は consumeSource / traceArgs の同期プレフィックスで同期実行される
6503
+ * ため、そこで (a) 他の stateElement(や自分自身のホスト)の同期切断、(b) 同一要素の
6504
+ * _state 同期再 set(clearStreamRegistry → startStreams で Set に再 add される)が
6505
+ * 起こり得る。(a) は切断済み要素への startStream が rootNode 不在で throw する経路、
6506
+ * (b) は registry から置換済みの旧 entry を restart して到達不能な孤児 consume run を
6507
+ * リークする経路(§3-2「未接続の stateElement の entry は restart しない」・
6508
+ * §5-1「切断後は idle」に違反)で、いずれも「entry が現行 registry の live entry で
6509
+ * あること」の再検証で skip する。startStream **実行中**の自己切断・再 set は事前
6510
+ * チェックではガードできないため、catch 側でも同じ再検証を行ってから error に
6511
+ * 正規化する(切断済みでの正規化は createState が再 throw して drain リスナー外へ
6512
+ * 漏れ、後続 hits の restart を巻き添えにするため)。
6513
+ * - restart(startStream)は entry ごとに try/catch し、throw(args のユーザー例外・
6514
+ * Promise 同期契約違反等)は controller.abort() → status="error"・$streamError 格納
6515
+ * に正規化する(§3-2 規範 3)。updater の drain を壊さず、他 entry の restart も
6516
+ * 継続する。eager 起動(connect 時の startStreams)の throw は従来どおり loud fail。
6517
+ * - restart 内の書き込み(initial リセット・status 通知)は updater への enqueue のみで
6518
+ * 新しい microtask バッチを作る(drain 再入ではない)。自己依存は traceArgs が
6519
+ * 宣言時に raiseError で検出するため、restart 書き込みが自分の依存に再 hit する
6520
+ * ループは起きない(§3-1)。
6521
+ */
6522
+ function restartStreamsOnUpdateBatch(batch) {
6523
+ const activeStateElements = getActiveStateElements();
6524
+ if (activeStateElements.size === 0) {
6525
+ // stream 未使用アプリの drain に配列・イテレータ割り当てのコストを載せない
6526
+ return;
6527
+ }
6528
+ const hits = [];
6529
+ for (const stateElement of activeStateElements) {
6530
+ for (const entry of getStreamEntries(stateElement).values()) {
6531
+ for (const dep of entry.depAddresses) {
6532
+ if (batch.has(dep)) {
6533
+ hits.push({ stateElement, entry });
6534
+ break;
6535
+ }
6536
+ }
6537
+ }
6538
+ }
6539
+ for (const { stateElement, entry } of hits) {
6540
+ // 先行 restart の source / args 同期実行は他要素の切断や同一要素の _state 同期再 set を
6541
+ // 行い得るため、実行時に再チェックする(live な Set / registry ビューで即時反映):
6542
+ // - 切断済み要素は skip(§3-2「未接続の stateElement の entry は restart しない」)
6543
+ // - entry が現行 registry のものでなければ skip — 同期再 set で置換された旧 entry を
6544
+ // restart すると、registry から到達不能なため abortAllStreams でも止められない
6545
+ // 孤児 consume run がリークする
6546
+ if (!activeStateElements.has(stateElement) ||
6547
+ getStreamEntries(stateElement).get(entry.name) !== entry) {
6548
+ continue;
6549
+ }
6550
+ try {
6551
+ startStream(stateElement, entry);
6552
+ }
6553
+ catch (e) {
6554
+ entry.controller?.abort();
6555
+ // startStream 実行中(args / source の同期プレフィックス)の自己切断・同期再 set は
6556
+ // 上の再チェックではガードできない。切断済みだと updateStreamStatus の createState が
6557
+ // rootNode 不在で再 throw して drain リスナー外へ漏れる(後続 hits の restart を
6558
+ // 巻き添えにする)ため、entry がまだ現行の live entry である場合のみ error に
6559
+ // 正規化する(切断済みなら abortAllStreams が idle に戻し済み。§3-2 規範 3 / §5-1)。
6560
+ if (activeStateElements.has(stateElement) &&
6561
+ getStreamEntries(stateElement).get(entry.name) === entry) {
6562
+ updateStreamStatus(stateElement, entry, "error", e);
6563
+ }
6564
+ }
6565
+ }
6566
+ }
6567
+ registerUpdateBatchListener(restartStreamsOnUpdateBatch);
6568
+
5626
6569
  function getterFn(name) {
5627
6570
  return function () {
5628
6571
  const stateEl = this.stateElement;
@@ -5981,19 +6924,42 @@ function checkDependency(handler, address) {
5981
6924
  * - ワイルドカードや多重ループにも柔軟に対応し、再帰的な値取得を実現
5982
6925
  * - finallyでキャッシュへの格納を保証
5983
6926
  */
5984
- function _getByAddress(target, address, receiver, handler, stateElement) {
5985
- if (address.pathInfo.segments[0] === STATE_COMMAND_NAMESPACE_NAME) {
5986
- // $command 名前空間配下のパスは raw state を持たないため、proxy の get トラップと
5987
- // 同じ namespace を辿る。1セグメント目は namespace 本体、2セグメント目以降は
5988
- // namespace 上のキー (= 宣言済み command token 名) を順に走査する。
5989
- let value = getCommandNamespace(stateElement);
5990
- for (let i = 1; i < address.pathInfo.segments.length; i++) {
5991
- if (value == null) {
5992
- return undefined;
5993
- }
5994
- value = Reflect.get(value, address.pathInfo.segments[i]);
6927
+ /**
6928
+ * namespace 配下のパスは raw state を持たないため、proxy の get トラップと同じ
6929
+ * namespace オブジェクトを辿る。1セグメント目は namespace 本体、2セグメント目以降は
6930
+ * namespace 上のキーを順に走査する。走査値が object / function 以外(null /
6931
+ * undefined / primitive の葉)になったら undefined を返す — 葉より深い読み
6932
+ * (例: `$streamStatus.<name>.<key>`、error が primitive throw のときの
6933
+ * `$streamError.<name>.message`)は宣言外アクセスと同じ undefined 解決とし、
6934
+ * Reflect.get non-object TypeError を updater の drain に漏らさない
6935
+ * (§4-1 の throw しない寛容規約)。
6936
+ */
6937
+ function walkNamespace(namespace, segments) {
6938
+ let value = namespace;
6939
+ for (let i = 1; i < segments.length; i++) {
6940
+ // Object(v) !== v は「v が object / function でない」(= primitive / null / undefined)判定
6941
+ if (Object(value) !== value) {
6942
+ return undefined;
5995
6943
  }
5996
- return value;
6944
+ value = Reflect.get(value, segments[i]);
6945
+ }
6946
+ return value;
6947
+ }
6948
+ function _getByAddress(target, address, receiver, handler, stateElement) {
6949
+ const firstSegment = address.pathInfo.segments[0];
6950
+ if (firstSegment === STATE_COMMAND_NAMESPACE_NAME) {
6951
+ // $command 名前空間: キーは宣言済み command token 名
6952
+ return walkNamespace(getCommandNamespace(stateElement), address.pathInfo.segments);
6953
+ }
6954
+ if (firstSegment === STATE_STREAM_STATUS_NAMESPACE_NAME) {
6955
+ // $streamStatus / $streamError 名前空間: キーは宣言済み stream 名
6956
+ // (registry entry が正本の thin gateway、docs/state-streams-design.md §4-2)。
6957
+ // setByAddress の親走査もここを通るため、子への Reflect.set が namespace proxy の
6958
+ // raiseError に到達する = 書き込み防御(S11)もこの分岐で成立する。
6959
+ return walkNamespace(getStreamStatusNamespace(stateElement), address.pathInfo.segments);
6960
+ }
6961
+ if (firstSegment === STATE_STREAM_ERROR_NAMESPACE_NAME) {
6962
+ return walkNamespace(getStreamErrorNamespace(stateElement), address.pathInfo.segments);
5997
6963
  }
5998
6964
  if (address.pathInfo.path in target) {
5999
6965
  // getterの中で参照の可能性があるので、addressをプッシュする
@@ -6039,6 +7005,8 @@ function _getByAddressWithCache(target, address, receiver, handler, stateElement
6039
7005
  }
6040
7006
  function getByAddress(target, address, receiver, handler) {
6041
7007
  checkDependency(handler, address);
7008
+ // $streams の args トレース中のみ絶対アドレスを捕捉(collector 非活性なら即 return)
7009
+ collectStreamDependency(handler.stateElement, address);
6042
7010
  const stateElement = handler.stateElement;
6043
7011
  const cacheable = address.pathInfo.wildcardCount > 0 ||
6044
7012
  stateElement.getterPaths.has(address.pathInfo.path);
@@ -6082,49 +7050,6 @@ function getContextListIndex(handler, structuredPath) {
6082
7050
  return address.listIndex?.at(index) ?? null;
6083
7051
  }
6084
7052
 
6085
- class Updater {
6086
- _queueAbsoluteAddresses = [];
6087
- constructor() {
6088
- }
6089
- enqueueAbsoluteAddress(absoluteAddress) {
6090
- const requireStartProcess = this._queueAbsoluteAddresses.length === 0;
6091
- this._queueAbsoluteAddresses.push(absoluteAddress);
6092
- if (requireStartProcess) {
6093
- queueMicrotask(() => {
6094
- const absoluteAddresses = this._queueAbsoluteAddresses;
6095
- this._queueAbsoluteAddresses = [];
6096
- this._applyChange(absoluteAddresses);
6097
- });
6098
- }
6099
- }
6100
- // テスト用に公開
6101
- testApplyChange(absoluteAddresses) {
6102
- this._applyChange(absoluteAddresses);
6103
- }
6104
- _applyChange(absoluteAddresses) {
6105
- // Note: AbsoluteStateAddress はキャッシュされているため、
6106
- // 同一の (stateName, address) は同じインスタンスとなり、
6107
- // Set による重複排除が正しく機能する
6108
- const absoluteAddressSet = new Set(absoluteAddresses);
6109
- const processBindings = [];
6110
- for (const absoluteAddress of absoluteAddressSet) {
6111
- const bindings = getBindingSetByAbsoluteStateAddress(absoluteAddress);
6112
- for (const binding of bindings) {
6113
- if (binding.replaceNode.isConnected === false) {
6114
- // 切断されているバインディングは無視
6115
- continue;
6116
- }
6117
- processBindings.push(binding);
6118
- }
6119
- }
6120
- applyChangeFromBindings(processBindings);
6121
- }
6122
- }
6123
- const updater = new Updater();
6124
- function getUpdater() {
6125
- return updater;
6126
- }
6127
-
6128
7053
  const swapInfoByStateAddress = new WeakMap();
6129
7054
  function getSwapInfoByAddress(address) {
6130
7055
  return swapInfoByStateAddress.get(address) ?? null;
@@ -6821,6 +7746,9 @@ async function setLoopContextAsync(handler, loopContext, callback) {
6821
7746
  * - 通常のプロパティアクセスもバインディングや多重ループに対応
6822
7747
  * - シンボルAPIやReflect.getで拡張性・互換性も確保
6823
7748
  */
7749
+ // `$streamStatus.<name>` / `$streamError.<name>` の dotted パス判定用プレフィックス
7750
+ const STREAM_STATUS_PATH_PREFIX = `${STATE_STREAM_STATUS_NAMESPACE_NAME}${DELIMITER}`;
7751
+ const STREAM_ERROR_PATH_PREFIX = `${STATE_STREAM_ERROR_NAMESPACE_NAME}${DELIMITER}`;
6824
7752
  function get(target, prop, receiver, handler) {
6825
7753
  const index = INDEX_BY_INDEX_NAME[prop];
6826
7754
  if (typeof index !== "undefined") {
@@ -6859,14 +7787,27 @@ function get(target, prop, receiver, handler) {
6859
7787
  case STATE_COMMAND_NAMESPACE_NAME: {
6860
7788
  return getCommandNamespace(handler.stateElement);
6861
7789
  }
7790
+ case STATE_STREAM_STATUS_NAMESPACE_NAME: {
7791
+ return getStreamStatusNamespace(handler.stateElement);
7792
+ }
7793
+ case STATE_STREAM_ERROR_NAMESPACE_NAME: {
7794
+ return getStreamErrorNamespace(handler.stateElement);
7795
+ }
7796
+ }
7797
+ // switch 不一致の $ プロパティのうち、`$streamStatus.<name>` / `$streamError.<name>`
7798
+ // の dotted パスだけは通常のパス解決(getByAddress)へフォールスルーさせる。
7799
+ // これが computed(getter)内での依存追跡付き読み取りの正規形
7800
+ // (checkDependency が getter スコープで動的依存を登録し、$postUpdate の
7801
+ // walkDependency で computed が無効化される、docs/state-streams-design.md §4-3)。
7802
+ // それ以外の未知 $ プロパティは従来どおり undefined を返す。
7803
+ if (!prop.startsWith(STREAM_STATUS_PATH_PREFIX) && !prop.startsWith(STREAM_ERROR_PATH_PREFIX)) {
7804
+ return undefined;
6862
7805
  }
6863
7806
  }
6864
- else {
6865
- const resolvedAddress = getResolvedAddress(prop);
6866
- const listIndex = getListIndex(target, resolvedAddress, receiver, handler);
6867
- const stateAddress = createStateAddress(resolvedAddress.pathInfo, listIndex);
6868
- return getByAddress(target, stateAddress, receiver, handler);
6869
- }
7807
+ const resolvedAddress = getResolvedAddress(prop);
7808
+ const listIndex = getListIndex(target, resolvedAddress, receiver, handler);
7809
+ const stateAddress = createStateAddress(resolvedAddress.pathInfo, listIndex);
7810
+ return getByAddress(target, stateAddress, receiver, handler);
6870
7811
  }
6871
7812
  else if (typeof prop === "symbol") {
6872
7813
  switch (prop) {
@@ -7466,6 +8407,17 @@ class State extends HTMLElement {
7466
8407
  _bindableEventMap = {};
7467
8408
  _commandTokenNames = new Set();
7468
8409
  _eventTokenNames = new Set();
8410
+ _dcc = false;
8411
+ // connect サイクルの世代カウンタ(connectedCallback 冒頭でインクリメント)。
8412
+ // $connectedCallback の await 中の「切断 → 即再接続」では、新 connect が
8413
+ // _rootNode を再設定済みのため陳腐化した旧 connect の再開が _rootNode ガードを
8414
+ // 素通りして startStreams に到達し、同一の再接続に対して source が二重起動する。
8415
+ // 末尾で冒頭に捕捉した世代と照合し、陳腐 connect からの起動を skip する(設計書 §2-3)。
8416
+ _connectGeneration = 0;
8417
+ // _state セッター側の startStreams が走った connect 世代
8418
+ // (connectedCallback 末尾の startStreams との二重起動防止、設計書 §2-3。
8419
+ // 世代が進めば不一致となり自然に無効化される — サイクル単位のフラグリセット相当)
8420
+ _streamsStartedGeneration = 0;
7469
8421
  constructor() {
7470
8422
  super();
7471
8423
  this._initializePromise = new Promise((resolve) => {
@@ -7497,6 +8449,9 @@ class State extends HTMLElement {
7497
8449
  this._listPaths.clear();
7498
8450
  this._elementPaths.clear();
7499
8451
  this._getterPaths.clear();
8452
+ // 再 set 時の残骸が $streams の衝突検査(processStreamsDeclaration)に
8453
+ // 偽陽性で命中しないよう getterPaths と対称にクリアする。
8454
+ this._setterPaths.clear();
7500
8455
  this._pathSet.clear();
7501
8456
  const stateInfo = getStateInfo(value);
7502
8457
  for (const path of stateInfo.getterPaths) {
@@ -7505,6 +8460,25 @@ class State extends HTMLElement {
7505
8460
  for (const path of stateInfo.setterPaths) {
7506
8461
  this._setterPaths.add(path);
7507
8462
  }
8463
+ // $streams: 再 set 時の二重起動防止のため旧 stream を abort + registry 全削除してから
8464
+ // 新宣言をパースする(clearEventTokenRegistry → processOnDeclaration と同じ再配線パターン)。
8465
+ // getterPaths / setterPaths の収集後であること(宣言バリデーションが衝突検査で参照する)。
8466
+ // namespace proxy の memo も破棄して古い proxy を捨てる(clearCommandNamespace と対称)。
8467
+ clearStreamNamespace(this);
8468
+ clearStreamRegistry(this);
8469
+ processStreamsDeclaration(this, value);
8470
+ // 接続中の再 set(S13)は新宣言で即再起動する。
8471
+ // 初回(_initialize 中)は _initialized が false なのでここでは起動されず、
8472
+ // connectedCallback 側の startStreams($connectedCallback 完了後)が担う。
8473
+ if (this._initialized && this._rootNode !== null && !inSsr()) {
8474
+ startStreams(this);
8475
+ // $connectedCallback 実行中の再 set(setInitialState)では、ここで新宣言が
8476
+ // 起動済みのため connectedCallback 末尾の startStreams を skip させる。
8477
+ // skip しないと同一 connect サイクルで新宣言の source が 2 回起動する
8478
+ // (1 回目は即 abort — switchMap 意味論で状態は壊れないが、副作用を持つ
8479
+ // source が 2 回発火してしまう)。
8480
+ this._streamsStartedGeneration = this._connectGeneration;
8481
+ }
7508
8482
  this._resolveLoading?.();
7509
8483
  }
7510
8484
  get name() {
@@ -7655,6 +8629,7 @@ class State extends HTMLElement {
7655
8629
  raiseError(`DCC: Failed to load state: ${e}`);
7656
8630
  }
7657
8631
  defineDCC(hostElement, shadowRoot, state);
8632
+ this._dcc = true;
7658
8633
  this._initialized = true;
7659
8634
  this._rootNode = null; // disconnectedCallbackでのstate参照を防止
7660
8635
  this._resolveInitialize?.();
@@ -7670,6 +8645,11 @@ class State extends HTMLElement {
7670
8645
  }
7671
8646
  async connectedCallback() {
7672
8647
  this._rootNode = this.getRootNode();
8648
+ // connect 世代を進めて冒頭で捕捉する(末尾の startStreams 前に照合し、
8649
+ // $connectedCallback の await 中に「切断 → 即再接続」された陳腐 connect の
8650
+ // 再開からの起動を防ぐ)。前回接続中の再 set(S13)で立った
8651
+ // _streamsStartedGeneration も世代不一致となり自然に無効化される。
8652
+ const connectGeneration = ++this._connectGeneration;
7673
8653
  if (!this._initialized) {
7674
8654
  // DCC 検出: ShadowRoot 内かつホストに data-wc-definition がある場合
7675
8655
  const parentNode = this.parentNode;
@@ -7683,6 +8663,12 @@ class State extends HTMLElement {
7683
8663
  this._initialized = true;
7684
8664
  this._resolveInitialize?.();
7685
8665
  }
8666
+ else if (!this._dcc && getStateElementByName(this._rootNode, this._name) !== this) {
8667
+ // 再接続(disconnect で名前登録が解除された後の再 connect): 登録を復元する。
8668
+ // createState が rootNode 経由でこの要素を解決できるようにするために必要
8669
+ // ($connectedCallback の再実行と $streams の initial からの再起動が依存する、設計書 §2-3)。
8670
+ setStateElementByName(this._rootNode, this._name, this);
8671
+ }
7686
8672
  // enable-ssr (クライアント側): SSR で $connectedCallback 済みなのでスキップ
7687
8673
  // inSsr() (サーバー側): レンダリング中なので実行する
7688
8674
  if (!this.hasAttribute('enable-ssr') || inSsr()) {
@@ -7699,16 +8685,53 @@ class State extends HTMLElement {
7699
8685
  Ssr.buildContent(ssrEl, stateData);
7700
8686
  this.parentNode?.insertBefore(ssrEl, this);
7701
8687
  }
8688
+ // $streams の eager 起動($connectedCallback 完了後、設計書 §2-3)。
8689
+ // inSsr() 時は起動しない(SSR 出力には initial が乗る、§7-1)。
8690
+ // enable-ssr のクライアント側は $connectedCallback をスキップしても起動する
8691
+ // (stream はシリアライズ不能なランタイム副作用のため)。
8692
+ // _rootNode ガード: $connectedCallback の await 中に切断された場合は起動しない。
8693
+ // ガードなしだと startStream 内の createState が rootNode 解決(disconnectedCallback
8694
+ // で null 化済み)の raiseError で throw し、connectedCallbackPromise が永遠に
8695
+ // 未解決になる。「未接続の entry は restart しない」設計書 §3-2 とも整合し、
8696
+ // _state セッター側の startStreams 前ガード(_rootNode !== null)と対称。
8697
+ // 世代ガード(connectGeneration 照合): await 中に「切断 → 即再接続」された場合、
8698
+ // 新 connect が _rootNode を再設定済みで上のガードを素通りするため、世代不一致で
8699
+ // 陳腐化した connect の再開を検出して skip する。起動点が新 connect の末尾に
8700
+ // 一本化され、「$connectedCallback 完了後に起動」(S1)の順序保証も保たれる。
8701
+ // _streamsStartedGeneration ガード: $connectedCallback 内の setInitialState
8702
+ // (接続中の再 set)で _state セッター側が新宣言を起動済みの場合は skip する
8703
+ // (skip しないと同一 connect サイクルで source が 2 回起動する、設計書 §2-3)。
8704
+ if (!inSsr() &&
8705
+ this._rootNode !== null &&
8706
+ connectGeneration === this._connectGeneration &&
8707
+ this._streamsStartedGeneration !== connectGeneration) {
8708
+ startStreams(this);
8709
+ }
7702
8710
  this._resolveConnectedCallback?.();
7703
8711
  }
7704
8712
  disconnectedCallback() {
7705
8713
  if (this._rootNode !== null) {
7706
- this._callStateDisconnectedCallback();
7707
- setStateElementByName(this.rootNode, this._name, null);
7708
- clearCommandTokenRegistry(this);
7709
- clearCommandNamespace(this);
7710
- clearEventTokenRegistry(this);
7711
- this._rootNode = null;
8714
+ // try/finally: ユーザーの $disconnectedCallback が throw しても後続の後始末を
8715
+ // 必ず実行する。特に abortAllStreams が飛ぶと stream が消費を続け(ゾンビ I/O)、
8716
+ // activeStateElements の強参照残留で GC が妨げられ、切断済み要素が依存駆動
8717
+ // restart の対象にも残る(設計書 §3-2 / §5-1 違反)。throw 自体は従来どおり
8718
+ // 呼び出し元へ伝播させる(変わるのは後始末の保証のみ)。
8719
+ try {
8720
+ this._callStateDisconnectedCallback();
8721
+ }
8722
+ finally {
8723
+ setStateElementByName(this.rootNode, this._name, null);
8724
+ clearCommandTokenRegistry(this);
8725
+ clearCommandNamespace(this);
8726
+ clearEventTokenRegistry(this);
8727
+ // stream は abort のみで registry は保持する(再接続時に同じ宣言から
8728
+ // initial で再起動できる、設計書 §5-1 / §5-2)。
8729
+ // namespace proxy の memo は破棄する(clearCommandNamespace と対称。
8730
+ // registry は残るため再接続後の初回アクセスで同内容の proxy が再生成される)。
8731
+ abortAllStreams(this);
8732
+ clearStreamNamespace(this);
8733
+ this._rootNode = null;
8734
+ }
7712
8735
  }
7713
8736
  }
7714
8737
  get initializePromise() {