@wcstack/state 1.20.0 → 1.21.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
@@ -22,6 +22,14 @@ const _config = {
22
22
  locale: 'en',
23
23
  debug: false,
24
24
  enableMustache: true,
25
+ enableDirectionalInitialSync: true,
26
+ enablePropagationContext: true,
27
+ // Phase 5b の dev-time contract analyzer は意図的に explicit opt-in(既定 off)。
28
+ // wcstack は buildless / zero-config で NODE_ENV 相当の確実な dev/prod 判定が無く、
29
+ // hostname や minification の heuristic で auto-ON すると誤検出で prod にコストを
30
+ // 乗せうるため、dev 既定 ON は採らない。利用側が setConfig で明示有効化する
31
+ // (docs/architecture-hardening/10-defaulting-rollout-status.md §C)。
32
+ enableContractAnalyzer: false,
25
33
  sameValueGuard: true,
26
34
  };
27
35
  // backward compatible export (read-only usage)
@@ -60,6 +68,15 @@ function setConfig(partialConfig) {
60
68
  if (typeof partialConfig.enableMustache === "boolean") {
61
69
  _config.enableMustache = partialConfig.enableMustache;
62
70
  }
71
+ if (typeof partialConfig.enableDirectionalInitialSync === "boolean") {
72
+ _config.enableDirectionalInitialSync = partialConfig.enableDirectionalInitialSync;
73
+ }
74
+ if (typeof partialConfig.enablePropagationContext === "boolean") {
75
+ _config.enablePropagationContext = partialConfig.enablePropagationContext;
76
+ }
77
+ if (typeof partialConfig.enableContractAnalyzer === "boolean") {
78
+ _config.enableContractAnalyzer = partialConfig.enableContractAnalyzer;
79
+ }
63
80
  if (typeof partialConfig.sameValueGuard === "boolean") {
64
81
  _config.sameValueGuard = partialConfig.sameValueGuard;
65
82
  }
@@ -93,27 +110,13 @@ function resolveInitializedBinding(node) {
93
110
  bindingPromise.resolve();
94
111
  }
95
112
 
96
- function raiseError(message) {
97
- throw new Error(`[@wcstack/state] ${message}`);
98
- }
99
-
100
- function replaceToReplaceNode(bindingInfo) {
101
- const node = bindingInfo.node;
102
- const replaceNode = bindingInfo.replaceNode;
103
- if (node === replaceNode) {
104
- return;
105
- }
106
- if (node.parentNode === null) {
107
- // already replaced
108
- return;
109
- }
110
- node.parentNode.replaceChild(replaceNode, node);
111
- }
112
-
113
113
  const DELIMITER = '.';
114
114
  const WILDCARD = '*';
115
115
  const MAX_WILDCARD_DEPTH = 128;
116
116
  const MAX_LOOP_DEPTH = 128;
117
+ // 因果伝播(Phase 3)の 1 transaction あたり hop 上限。超過分の未処理 record は
118
+ // quarantine し(適用済みの値は戻さない)、updater から例外は投げない。
119
+ const MAX_PROPAGATION_HOPS = 32;
117
120
  // data-wcs バインディング構文 `[prop][#mod]: [path][@state][|filter...]` の区切り文字(単一正本)。
118
121
  // これらは「死守の壁(構文契約)」であり値は不変。manifest.syntax.delimiters で公開される。
119
122
  const BINDING_SEPARATOR = ';'; // 複数バインディングの区切り
@@ -283,14 +286,124 @@ function getCustomElement(node) {
283
286
  }
284
287
  }
285
288
 
286
- function isValidWcBindable(value) {
289
+ /**
290
+ * Resolve the registry at operation time so importing the runtime remains safe
291
+ * when browser globals are absent. The owner hook is reserved for scoped
292
+ * registries; current callers fall back to the global registry.
293
+ */
294
+ function getCustomElementRegistry(owner = null) {
295
+ const globalRegistry = globalThis.customElements;
296
+ const registry = owner?.customElements ?? globalRegistry;
297
+ if (typeof registry !== "object" || registry === null)
298
+ return null;
299
+ const candidate = registry;
300
+ if (typeof candidate.get !== "function" || typeof candidate.whenDefined !== "function") {
301
+ return null;
302
+ }
303
+ return candidate;
304
+ }
305
+ function upgradeCustomElement(registry, root) {
306
+ registry.upgrade?.(root);
307
+ }
308
+
309
+ // ===========================================================================
310
+ // AUTO-GENERATED FILE - DO NOT EDIT.
311
+ // Generated from /protocol/wc-bindable-reader.ts by scripts/sync-protocol-types.mjs.
312
+ // Run `node scripts/sync-protocol-types.mjs` after editing the source.
313
+ // ===========================================================================
314
+ const MIN_WC_BINDABLE_VERSION = 1;
315
+ /**
316
+ * Repository-local conformance mirror of @wc-bindable/core's
317
+ * getWcBindableDeclaration(). Discovery has one path only:
318
+ * target.constructor.wcBindable.
319
+ *
320
+ * The declaration remains live. The maps are read-time indexes and are not a
321
+ * clone, freeze, or normalized replacement for liveDeclaration.
322
+ */
323
+ function readBindableDeclaration(target) {
324
+ try {
325
+ if (target === null || (typeof target !== "object" && typeof target !== "function")) {
326
+ return null;
327
+ }
328
+ const candidate = target;
329
+ const addEventListener = candidate.addEventListener;
330
+ const removeEventListener = candidate.removeEventListener;
331
+ const declaration = candidate.constructor?.wcBindable;
332
+ if (typeof addEventListener !== "function" || typeof removeEventListener !== "function") {
333
+ return null;
334
+ }
335
+ if (declaration?.protocol !== "wc-bindable")
336
+ return null;
337
+ if (!Number.isInteger(declaration.version) || declaration.version < MIN_WC_BINDABLE_VERSION) {
338
+ return null;
339
+ }
340
+ const knownProperties = readNamedList(declaration.properties, isValidPropertyDescriptor);
341
+ if (knownProperties === null)
342
+ return null;
343
+ const declaredInputs = declaration.inputs === undefined
344
+ ? new Map()
345
+ : readNamedList(declaration.inputs, isValidInputDescriptor);
346
+ if (declaredInputs === null)
347
+ return null;
348
+ const declaredCommands = declaration.commands === undefined
349
+ ? new Map()
350
+ : readNamedList(declaration.commands, isValidCommandDescriptor);
351
+ if (declaredCommands === null)
352
+ return null;
353
+ return {
354
+ target: target,
355
+ liveDeclaration: declaration,
356
+ knownProperties,
357
+ declaredInputs,
358
+ declaredCommands,
359
+ };
360
+ }
361
+ catch {
362
+ return null;
363
+ }
364
+ }
365
+ function isValidPropertyDescriptor(value) {
366
+ if (typeof value !== "object" || value === null)
367
+ return false;
368
+ const descriptor = value;
369
+ if (typeof descriptor.name !== "string" || descriptor.name.length === 0)
370
+ return false;
371
+ if (typeof descriptor.event !== "string" || descriptor.event.length === 0)
372
+ return false;
373
+ return descriptor.getter === undefined || typeof descriptor.getter === "function";
374
+ }
375
+ function isValidInputDescriptor(value) {
376
+ if (typeof value !== "object" || value === null)
377
+ return false;
378
+ const descriptor = value;
379
+ if (typeof descriptor.name !== "string" || descriptor.name.length === 0)
380
+ return false;
381
+ return descriptor.attribute === undefined || typeof descriptor.attribute === "string";
382
+ }
383
+ function isValidCommandDescriptor(value) {
287
384
  if (typeof value !== "object" || value === null)
288
385
  return false;
289
- const v = value;
290
- return v.protocol === "wc-bindable"
291
- && v.version === 1
292
- && Array.isArray(v.properties);
386
+ const descriptor = value;
387
+ if (typeof descriptor.name !== "string" || descriptor.name.length === 0)
388
+ return false;
389
+ return descriptor.async === undefined || typeof descriptor.async === "boolean";
390
+ }
391
+ function readNamedList(value, isValidEntry) {
392
+ if (!Array.isArray(value))
393
+ return null;
394
+ const entries = new Map();
395
+ for (const entry of value) {
396
+ if (!isValidEntry(entry) || entries.has(entry.name))
397
+ return null;
398
+ entries.set(entry.name, entry);
399
+ }
400
+ return entries;
401
+ }
402
+
403
+ function raiseError(message) {
404
+ throw new Error(`[@wcstack/state] ${message}`);
293
405
  }
406
+
294
407
  function makeExpandedEntry(name, base, stateName) {
295
408
  // Dot-relative spread keeps the loop item root (`.`) without producing `..foo`.
296
409
  const expandedPath = base === "." ? `.${name}` : `${base}.${name}`;
@@ -366,7 +479,11 @@ function expandSpread(node, results, options = {}) {
366
479
  if (tagName === null) {
367
480
  raiseError(`Spread binding "${result.statePathName}" requires a custom element with wcBindable, but <${element.tagName.toLowerCase()}> is not a custom element.`);
368
481
  }
369
- const customClass = customElements.get(tagName);
482
+ const registry = getCustomElementRegistry();
483
+ if (registry === null) {
484
+ raiseError(`CustomElementRegistry is unavailable for <${tagName}>.`);
485
+ }
486
+ const customClass = registry.get(tagName);
370
487
  if (typeof customClass === "undefined") {
371
488
  if (!allowDeferred) {
372
489
  raiseError(`Spread binding "${result.statePathName}" requires <${tagName}> to be registered. Define the custom element before initializing this binding.`);
@@ -375,28 +492,29 @@ function expandSpread(node, results, options = {}) {
375
492
  expanded.push(result);
376
493
  continue;
377
494
  }
378
- const bindable = customClass.wcBindable;
379
- if (!isValidWcBindable(bindable)) {
380
- raiseError(`Spread binding "${result.statePathName}" requires <${tagName}> to declare wcBindable (protocol="wc-bindable", version=1).`);
495
+ upgradeCustomElement(registry, element);
496
+ const bindable = readBindableDeclaration(element);
497
+ if (bindable === null) {
498
+ raiseError(`Spread binding "${result.statePathName}" requires <${tagName}> to expose a valid wcBindable declaration.`);
381
499
  }
382
500
  const targetBase = result.statePathName;
383
501
  const stateName = result.stateName;
384
502
  const seen = new Set();
385
- for (const prop of bindable.properties) {
386
- if (seen.has(prop.name))
503
+ for (const name of bindable.knownProperties.keys()) {
504
+ if (seen.has(name))
387
505
  continue;
388
- seen.add(prop.name);
389
- const entry = makeExpandedEntry(prop.name, targetBase, stateName);
506
+ seen.add(name);
507
+ const entry = makeExpandedEntry(name, targetBase, stateName);
390
508
  spreadOrigin.add(entry);
391
509
  expanded.push(entry);
392
510
  }
393
511
  // properties win over inputs when the name overlaps because they carry the
394
512
  // full property contract (for example change events).
395
- for (const input of (bindable.inputs ?? [])) {
396
- if (seen.has(input.name))
513
+ for (const name of bindable.declaredInputs.keys()) {
514
+ if (seen.has(name))
397
515
  continue;
398
- seen.add(input.name);
399
- const entry = makeExpandedEntry(input.name, targetBase, stateName);
516
+ seen.add(name);
517
+ const entry = makeExpandedEntry(name, targetBase, stateName);
400
518
  spreadOrigin.add(entry);
401
519
  expanded.push(entry);
402
520
  }
@@ -1745,25 +1863,95 @@ function processDeferredNode(entry) {
1745
1863
  return result.bindings;
1746
1864
  }
1747
1865
 
1866
+ const loopContextByNode = new WeakMap();
1867
+ function getLoopContextByNode(node) {
1868
+ let paramNode = node;
1869
+ while (paramNode) {
1870
+ const loopContext = loopContextByNode.get(paramNode);
1871
+ if (loopContext) {
1872
+ return loopContext;
1873
+ }
1874
+ paramNode = paramNode.parentNode;
1875
+ }
1876
+ return null;
1877
+ }
1878
+ function setLoopContextByNode(node, loopContext) {
1879
+ if (loopContext === null) {
1880
+ loopContextByNode.delete(node);
1881
+ return;
1882
+ }
1883
+ loopContextByNode.set(node, loopContext);
1884
+ }
1885
+
1886
+ const lastListValueByAbsoluteStateAddress = new WeakMap();
1887
+ function getLastListValueByAbsoluteStateAddress(address) {
1888
+ return lastListValueByAbsoluteStateAddress.get(address) ?? [];
1889
+ }
1890
+ function setLastListValueByAbsoluteStateAddress(address, value) {
1891
+ lastListValueByAbsoluteStateAddress.set(address, value);
1892
+ }
1893
+
1894
+ const setLoopContextAsyncSymbol = Symbol("$$setLoopContextAsync");
1895
+ const setLoopContextSymbol = Symbol("$$setLoopContext");
1896
+ const getByAddressSymbol = Symbol("$$getByAddress");
1897
+ const hasByAddressSymbol = Symbol("$$hasByAddress");
1898
+ const setByAddressSymbol = Symbol("$$setByAddress");
1899
+ const connectedCallbackSymbol = Symbol("$$connectedCallback");
1900
+ const disconnectedCallbackSymbol = Symbol("$$disconnectedCallback");
1901
+ const updatedCallbackSymbol = Symbol("$$updatedCallback");
1902
+
1748
1903
  const _cache$3 = new WeakMap();
1749
- const _cacheNullListIndex$1 = new WeakMap();
1750
- class StateAddress {
1904
+ function getAbsolutePathInfo(stateElement, pathInfo) {
1905
+ if (_cache$3.has(stateElement)) {
1906
+ const pathMap = _cache$3.get(stateElement);
1907
+ if (pathMap.has(pathInfo)) {
1908
+ return pathMap.get(pathInfo);
1909
+ }
1910
+ }
1911
+ else {
1912
+ _cache$3.set(stateElement, new WeakMap());
1913
+ }
1914
+ const absolutePathInfo = Object.freeze(new AbsolutePathInfo(stateElement, pathInfo));
1915
+ _cache$3.get(stateElement).set(pathInfo, absolutePathInfo);
1916
+ return absolutePathInfo;
1917
+ }
1918
+ class AbsolutePathInfo {
1751
1919
  pathInfo;
1752
- listIndex;
1753
- _parentAddress;
1754
- constructor(pathInfo, listIndex) {
1920
+ stateName;
1921
+ stateElement;
1922
+ parentAbsolutePathInfo;
1923
+ constructor(stateElement, pathInfo) {
1755
1924
  this.pathInfo = pathInfo;
1925
+ this.stateName = stateElement.name;
1926
+ this.stateElement = stateElement;
1927
+ if (pathInfo.parentPathInfo === null) {
1928
+ this.parentAbsolutePathInfo = null;
1929
+ }
1930
+ else {
1931
+ this.parentAbsolutePathInfo = getAbsolutePathInfo(stateElement, pathInfo.parentPathInfo);
1932
+ }
1933
+ }
1934
+ }
1935
+
1936
+ const _cache$2 = new WeakMap();
1937
+ const _cacheNullListIndex$1 = new WeakMap();
1938
+ class AbsoluteStateAddress {
1939
+ absolutePathInfo;
1940
+ listIndex;
1941
+ _parentAbsoluteAddress;
1942
+ constructor(absolutePathInfo, listIndex) {
1943
+ this.absolutePathInfo = absolutePathInfo;
1756
1944
  this.listIndex = listIndex;
1757
1945
  }
1758
- get parentAddress() {
1759
- if (typeof this._parentAddress !== 'undefined') {
1760
- return this._parentAddress;
1946
+ get parentAbsoluteAddress() {
1947
+ if (typeof this._parentAbsoluteAddress !== 'undefined') {
1948
+ return this._parentAbsoluteAddress;
1761
1949
  }
1762
- const parentPathInfo = this.pathInfo.parentPathInfo;
1763
- if (parentPathInfo === null) {
1950
+ const parentAbsolutePathInfo = this.absolutePathInfo.parentAbsolutePathInfo;
1951
+ if (parentAbsolutePathInfo === null) {
1764
1952
  return null;
1765
1953
  }
1766
- const lastSegment = this.pathInfo.segments[this.pathInfo.segments.length - 1];
1954
+ const lastSegment = this.absolutePathInfo.pathInfo.segments[this.absolutePathInfo.pathInfo.segments.length - 1];
1767
1955
  let parentListIndex = null;
1768
1956
  if (lastSegment === WILDCARD) {
1769
1957
  parentListIndex = this.listIndex?.parentListIndex ?? null;
@@ -1771,148 +1959,276 @@ class StateAddress {
1771
1959
  else {
1772
1960
  parentListIndex = this.listIndex;
1773
1961
  }
1774
- return this._parentAddress = createStateAddress(parentPathInfo, parentListIndex);
1962
+ return this._parentAbsoluteAddress = createAbsoluteStateAddress(parentAbsolutePathInfo, parentListIndex);
1775
1963
  }
1776
1964
  }
1777
- function createStateAddress(pathInfo, listIndex) {
1965
+ function createAbsoluteStateAddress(absolutePathInfo, listIndex) {
1778
1966
  if (listIndex === null) {
1779
- let cached = _cacheNullListIndex$1.get(pathInfo);
1967
+ let cached = _cacheNullListIndex$1.get(absolutePathInfo);
1780
1968
  if (typeof cached !== "undefined") {
1781
1969
  return cached;
1782
1970
  }
1783
- cached = new StateAddress(pathInfo, null);
1784
- _cacheNullListIndex$1.set(pathInfo, cached);
1971
+ cached = new AbsoluteStateAddress(absolutePathInfo, null);
1972
+ _cacheNullListIndex$1.set(absolutePathInfo, cached);
1785
1973
  return cached;
1786
1974
  }
1787
1975
  else {
1788
- let cacheByPathInfo = _cache$3.get(listIndex);
1789
- if (typeof cacheByPathInfo === "undefined") {
1790
- cacheByPathInfo = new WeakMap();
1791
- _cache$3.set(listIndex, cacheByPathInfo);
1976
+ let cacheByAbsolutePathInfo = _cache$2.get(listIndex);
1977
+ if (typeof cacheByAbsolutePathInfo === "undefined") {
1978
+ cacheByAbsolutePathInfo = new WeakMap();
1979
+ _cache$2.set(listIndex, cacheByAbsolutePathInfo);
1792
1980
  }
1793
- let cached = cacheByPathInfo.get(pathInfo);
1981
+ let cached = cacheByAbsolutePathInfo.get(absolutePathInfo);
1794
1982
  if (typeof cached !== "undefined") {
1795
1983
  return cached;
1796
1984
  }
1797
- cached = new StateAddress(pathInfo, listIndex);
1798
- cacheByPathInfo.set(pathInfo, cached);
1985
+ cached = new AbsoluteStateAddress(absolutePathInfo, listIndex);
1986
+ cacheByAbsolutePathInfo.set(absolutePathInfo, cached);
1799
1987
  return cached;
1800
1988
  }
1801
1989
  }
1802
1990
 
1803
- /**
1804
- * devtools/sink.ts
1805
- *
1806
- * 計装点が参照するホットパス唯一の接点。依存ゼロの葉モジュールにすることで、
1807
- * 計装される側(stateElementByName / setByAddress / binding / token)と
1808
- * bridge の間の循環 import を避ける。
1809
- *
1810
- * コスト規範(protocol §1-1): フック未接続時、計装点のコストは
1811
- * `devtoolsSink !== null` の分岐 1 個。イベントオブジェクトの生成は
1812
- * 必ずこのチェックの内側で行うこと。
1813
- */
1814
- /** live binding としてエクスポート。計装点は `if (devtoolsSink !== null)` で参照する */
1815
- let devtoolsSink = null;
1816
- function setDevtoolsSink(sink) {
1817
- devtoolsSink = sink;
1991
+ const rootNodeByFragment = new WeakMap();
1992
+ function setRootNodeByFragment(fragment, rootNode) {
1993
+ if (rootNode === null) {
1994
+ rootNodeByFragment.delete(fragment);
1995
+ }
1996
+ else {
1997
+ rootNodeByFragment.set(fragment, rootNode);
1998
+ }
1999
+ }
2000
+ function getRootNodeByFragment(fragment) {
2001
+ return rootNodeByFragment.get(fragment) || null;
1818
2002
  }
1819
2003
 
1820
- // command-token / event-token が共有する pub/sub プリミティブ。
1821
- // _subscribers Set のため挿入順を保持する。
1822
- // emit() は subscribe() された順に呼び出され、戻り値配列も同じ順序で返る。
1823
- //
1824
- // 「誰が subscribe し誰が emit するか」だけが command / event の違い:
1825
- // - command-token: element が subscribe / state が emit
1826
- // - event-token: state(`$on`) が subscribe / element(listener) が emit
1827
- class Token {
1828
- _name;
1829
- _subscribers = new Set();
1830
- constructor(name) {
1831
- this._name = name;
1832
- }
1833
- get name() {
1834
- return this._name;
2004
+ const cacheCalcWildcardLen = new Map();
2005
+ function calcWildcardLen(pathInfo, targetPathInfo) {
2006
+ let path1;
2007
+ let path2;
2008
+ if (pathInfo.wildcardCount === 0 || targetPathInfo.wildcardCount === 0) {
2009
+ return 0;
1835
2010
  }
1836
- get size() {
1837
- return this._subscribers.size;
2011
+ if (pathInfo.wildcardCount === 1
2012
+ && targetPathInfo.wildcardCount > 0
2013
+ && targetPathInfo.wildcardPathSet.has(pathInfo.path)) {
2014
+ return 1;
1838
2015
  }
1839
- subscribe(fn) {
1840
- this._subscribers.add(fn);
1841
- return () => {
1842
- this._subscribers.delete(fn);
1843
- };
2016
+ if (pathInfo.id < targetPathInfo.id) {
2017
+ path1 = pathInfo;
2018
+ path2 = targetPathInfo;
1844
2019
  }
1845
- unsubscribe(fn) {
1846
- return this._subscribers.delete(fn);
2020
+ else {
2021
+ path1 = targetPathInfo;
2022
+ path2 = pathInfo;
1847
2023
  }
1848
- emit(...args) {
1849
- const results = [];
1850
- for (const fn of this._subscribers) {
1851
- results.push(fn(...args));
1852
- }
1853
- return results;
2024
+ const key = `${path1.path}\t${path2.path}`;
2025
+ let len = cacheCalcWildcardLen.get(key);
2026
+ if (typeof len !== "undefined") {
2027
+ return len;
1854
2028
  }
2029
+ const matchPath = path1.wildcardPathSet.intersection(path2.wildcardPathSet);
2030
+ len = matchPath.size;
2031
+ cacheCalcWildcardLen.set(key, len);
2032
+ return len;
1855
2033
  }
1856
2034
 
1857
- // CommandToken は共有 pub/sub プリミティブ Token の薄い特化。
1858
- // instanceof による型判別を成立させるため独立クラスとして維持する。
1859
- //
1860
- // ownerStateName devtools 計装(protocol §4.5)のための内部 optional 引数。
1861
- // command-token-protocol の外部仕様は不変更(registry が渡すだけで、
1862
- // subscribe/emit の意味論には一切影響しない)。
1863
- class CommandToken extends Token {
1864
- _ownerStateName;
1865
- constructor(name, ownerStateName) {
1866
- super(name);
1867
- this._ownerStateName = ownerStateName ?? null;
2035
+ const listIndexByBindingInfoByLoopContext = new WeakMap();
2036
+ function getListIndexByBindingInfo(bindingInfo) {
2037
+ const loopContext = getLoopContextByNode(bindingInfo.node);
2038
+ if (loopContext === null) {
2039
+ return null;
1868
2040
  }
1869
- emit(...args) {
1870
- if (devtoolsSink !== null) {
1871
- // subscriberCount 0 の emit(空撃ち)もそのまま流す — whenDefined 前の
1872
- // command 空撃ちレース類をタイムラインで可視化するため
1873
- devtoolsSink({
1874
- type: "state:token-emit",
1875
- kind: "command",
1876
- stateName: this._ownerStateName,
1877
- tokenName: this.name,
1878
- args,
1879
- subscriberCount: this.size,
1880
- });
2041
+ let listIndexByBindingInfo = listIndexByBindingInfoByLoopContext.get(loopContext);
2042
+ if (typeof listIndexByBindingInfo === "undefined") {
2043
+ listIndexByBindingInfo = new WeakMap();
2044
+ listIndexByBindingInfoByLoopContext.set(loopContext, listIndexByBindingInfo);
2045
+ }
2046
+ else {
2047
+ const listIndex = listIndexByBindingInfo.get(bindingInfo);
2048
+ if (typeof listIndex !== "undefined") {
2049
+ return listIndex;
1881
2050
  }
1882
- return super.emit(...args);
2051
+ }
2052
+ let listIndex = null;
2053
+ try {
2054
+ const wildcardLen = calcWildcardLen(loopContext.pathInfo, bindingInfo.statePathInfo);
2055
+ if (wildcardLen > 0) {
2056
+ listIndex = loopContext.listIndex.at(wildcardLen - 1);
2057
+ }
2058
+ return listIndex;
2059
+ }
2060
+ finally {
2061
+ listIndexByBindingInfo.set(bindingInfo, listIndex);
1883
2062
  }
1884
2063
  }
1885
- function isCommandToken(value) {
1886
- return value instanceof CommandToken;
2064
+
2065
+ const absoluteStateAddressByBinding = new WeakMap();
2066
+ function getAbsoluteStateAddressByBinding(binding) {
2067
+ // 切断されていても、キャッシュされていれば絶対状態アドレスを返す。
2068
+ let absoluteStateAddress = null;
2069
+ absoluteStateAddress = absoluteStateAddressByBinding.get(binding) || null;
2070
+ if (absoluteStateAddress !== null) {
2071
+ return absoluteStateAddress;
2072
+ }
2073
+ let rootNode = binding.replaceNode.getRootNode();
2074
+ // binding.replaceNodeはisConnected=trueになっていることが前提、切断されている場合はraiseErrorを返す
2075
+ if (binding.replaceNode.isConnected === false) {
2076
+ // DocumentFragmentでバッファリングされている場合は、ルートノードをDocumentFragmentから実際のルートノードに切り替える
2077
+ const rootNodeByFragment = getRootNodeByFragment(rootNode);
2078
+ if (rootNodeByFragment === null) {
2079
+ raiseError(`Cannot get absolute state address for disconnected binding: ${binding.bindingType} ${binding.statePathName} on ${binding.node.nodeName}`);
2080
+ }
2081
+ else {
2082
+ rootNode = rootNodeByFragment;
2083
+ }
2084
+ }
2085
+ const listIndex = getListIndexByBindingInfo(binding);
2086
+ const stateElement = getStateElementByName(rootNode, binding.stateName);
2087
+ if (stateElement === null) {
2088
+ raiseError(`State element with name "${binding.stateName}" not found for binding.`);
2089
+ }
2090
+ const absolutePathInfo = getAbsolutePathInfo(stateElement, binding.statePathInfo);
2091
+ absoluteStateAddress =
2092
+ createAbsoluteStateAddress(absolutePathInfo, listIndex);
2093
+ absoluteStateAddressByBinding.set(binding, absoluteStateAddress);
2094
+ return absoluteStateAddress;
2095
+ }
2096
+ function clearAbsoluteStateAddressByBinding(binding) {
2097
+ absoluteStateAddressByBinding.delete(binding);
1887
2098
  }
1888
2099
 
1889
- const loopContextByNode = new WeakMap();
1890
- function getLoopContextByNode(node) {
1891
- let paramNode = node;
1892
- while (paramNode) {
1893
- const loopContext = loopContextByNode.get(paramNode);
1894
- if (loopContext) {
1895
- return loopContext;
2100
+ /**
2101
+ * devtools/sink.ts
2102
+ *
2103
+ * 計装点が参照するホットパス唯一の接点。依存ゼロの葉モジュールにすることで、
2104
+ * 計装される側(stateElementByName / setByAddress / binding / token)と
2105
+ * bridge の間の循環 import を避ける。
2106
+ *
2107
+ * コスト規範(protocol §1-1): フック未接続時、計装点のコストは
2108
+ * `devtoolsSink !== null` の分岐 1 個。イベントオブジェクトの生成は
2109
+ * 必ずこのチェックの内側で行うこと。
2110
+ */
2111
+ /** live binding としてエクスポート。計装点は `if (devtoolsSink !== null)` で参照する */
2112
+ let devtoolsSink = null;
2113
+ function setDevtoolsSink(sink) {
2114
+ devtoolsSink = sink;
2115
+ }
2116
+
2117
+ const bindingSetByAbsoluteStateAddress = new WeakMap();
2118
+ function getBindingSetByAbsoluteStateAddress(absoluteStateAddress) {
2119
+ let bindingSet = null;
2120
+ bindingSet = bindingSetByAbsoluteStateAddress.get(absoluteStateAddress) || null;
2121
+ if (bindingSet === null) {
2122
+ bindingSet = new Set();
2123
+ bindingSetByAbsoluteStateAddress.set(absoluteStateAddress, bindingSet);
2124
+ }
2125
+ return bindingSet;
2126
+ }
2127
+ /**
2128
+ * 参照専用の取得。get-or-create と違い、未登録アドレスに空 Set を
2129
+ * 生成・キャッシュしない(リスト置換の drain は大量のバインディング無し
2130
+ * アドレスを照会するため、生成すると空 Set が溜まり続ける)。
2131
+ */
2132
+ function peekBindingSetByAbsoluteStateAddress(absoluteStateAddress) {
2133
+ return bindingSetByAbsoluteStateAddress.get(absoluteStateAddress);
2134
+ }
2135
+ function addBindingByAbsoluteStateAddress(absoluteStateAddress, binding) {
2136
+ const bindingSet = getBindingSetByAbsoluteStateAddress(absoluteStateAddress);
2137
+ bindingSet.add(binding);
2138
+ if (devtoolsSink !== null) {
2139
+ devtoolsSink({ type: "state:binding-added", absoluteAddress: absoluteStateAddress, binding });
2140
+ }
2141
+ }
2142
+ function removeBindingByAbsoluteStateAddress(absoluteStateAddress, binding) {
2143
+ // get-or-create を通すと未登録アドレスに空 Set を生成してしまうため素の get で参照する
2144
+ const bindingSet = bindingSetByAbsoluteStateAddress.get(absoluteStateAddress);
2145
+ if (bindingSet !== undefined) {
2146
+ bindingSet.delete(binding);
2147
+ if (devtoolsSink !== null) {
2148
+ devtoolsSink({ type: "state:binding-removed", absoluteAddress: absoluteStateAddress, binding });
1896
2149
  }
1897
- paramNode = paramNode.parentNode;
1898
2150
  }
1899
- return null;
1900
2151
  }
1901
- function setLoopContextByNode(node, loopContext) {
1902
- if (loopContext === null) {
1903
- loopContextByNode.delete(node);
1904
- return;
2152
+
2153
+ const _cache$1 = new WeakMap();
2154
+ const _cacheNullListIndex = new WeakMap();
2155
+ class StateAddress {
2156
+ pathInfo;
2157
+ listIndex;
2158
+ _parentAddress;
2159
+ constructor(pathInfo, listIndex) {
2160
+ this.pathInfo = pathInfo;
2161
+ this.listIndex = listIndex;
2162
+ }
2163
+ get parentAddress() {
2164
+ if (typeof this._parentAddress !== 'undefined') {
2165
+ return this._parentAddress;
2166
+ }
2167
+ const parentPathInfo = this.pathInfo.parentPathInfo;
2168
+ if (parentPathInfo === null) {
2169
+ return null;
2170
+ }
2171
+ const lastSegment = this.pathInfo.segments[this.pathInfo.segments.length - 1];
2172
+ let parentListIndex = null;
2173
+ if (lastSegment === WILDCARD) {
2174
+ parentListIndex = this.listIndex?.parentListIndex ?? null;
2175
+ }
2176
+ else {
2177
+ parentListIndex = this.listIndex;
2178
+ }
2179
+ return this._parentAddress = createStateAddress(parentPathInfo, parentListIndex);
2180
+ }
2181
+ }
2182
+ function createStateAddress(pathInfo, listIndex) {
2183
+ if (listIndex === null) {
2184
+ let cached = _cacheNullListIndex.get(pathInfo);
2185
+ if (typeof cached !== "undefined") {
2186
+ return cached;
2187
+ }
2188
+ cached = new StateAddress(pathInfo, null);
2189
+ _cacheNullListIndex.set(pathInfo, cached);
2190
+ return cached;
2191
+ }
2192
+ else {
2193
+ let cacheByPathInfo = _cache$1.get(listIndex);
2194
+ if (typeof cacheByPathInfo === "undefined") {
2195
+ cacheByPathInfo = new WeakMap();
2196
+ _cache$1.set(listIndex, cacheByPathInfo);
2197
+ }
2198
+ let cached = cacheByPathInfo.get(pathInfo);
2199
+ if (typeof cached !== "undefined") {
2200
+ return cached;
2201
+ }
2202
+ cached = new StateAddress(pathInfo, listIndex);
2203
+ cacheByPathInfo.set(pathInfo, cached);
2204
+ return cached;
1905
2205
  }
1906
- loopContextByNode.set(node, loopContext);
1907
2206
  }
1908
2207
 
1909
- const setLoopContextAsyncSymbol = Symbol("$$setLoopContextAsync");
1910
- const setLoopContextSymbol = Symbol("$$setLoopContext");
1911
- const getByAddressSymbol = Symbol("$$getByAddress");
1912
- const setByAddressSymbol = Symbol("$$setByAddress");
1913
- const connectedCallbackSymbol = Symbol("$$connectedCallback");
1914
- const disconnectedCallbackSymbol = Symbol("$$disconnectedCallback");
1915
- const updatedCallbackSymbol = Symbol("$$updatedCallback");
2208
+ const stateAddressByBindingInfo = new WeakMap();
2209
+ function getStateAddressByBindingInfo(bindingInfo) {
2210
+ let stateAddress = null;
2211
+ stateAddress = stateAddressByBindingInfo.get(bindingInfo) || null;
2212
+ if (stateAddress !== null) {
2213
+ return stateAddress;
2214
+ }
2215
+ if (bindingInfo.statePathInfo.wildcardCount > 0) {
2216
+ const listIndex = getListIndexByBindingInfo(bindingInfo);
2217
+ if (listIndex === null) {
2218
+ raiseError(`Cannot resolve state address for binding with wildcard statePathName "${bindingInfo.statePathName}" because list index is null.`);
2219
+ }
2220
+ stateAddress = createStateAddress(bindingInfo.statePathInfo, listIndex);
2221
+ }
2222
+ else {
2223
+ stateAddress = createStateAddress(bindingInfo.statePathInfo, null);
2224
+ }
2225
+ stateAddressByBindingInfo.set(bindingInfo, stateAddress);
2226
+ return stateAddress;
2227
+ }
2228
+ // call for change loopContext
2229
+ function clearStateAddressByBindingInfo(bindingInfo) {
2230
+ stateAddressByBindingInfo.delete(bindingInfo);
2231
+ }
1916
2232
 
1917
2233
  function createHandlerBindingRegistry() {
1918
2234
  const attachedByKey = new Map();
@@ -1962,64 +2278,141 @@ function createHandlerBindingRegistry() {
1962
2278
  };
1963
2279
  }
1964
2280
 
1965
- // onclick: $command.<name> のように、DOM イベントから command token を直接 emit する形式かを判定する。
1966
- // 右辺が $command 名前空間配下のパス($command.<token>)のときに true。
1967
- function isCommandTokenPath(statePathName) {
1968
- return statePathName.startsWith(STATE_COMMAND_NAMESPACE_NAME + ".");
1969
- }
1970
2281
  const handlerByHandlerKey$3 = new Map();
1971
2282
  // binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
1972
2283
  const bindingRegistry$3 = createHandlerBindingRegistry();
1973
- function getHandlerKey$3(binding) {
1974
- const modifierKey = binding.propModifiers.filter(m => m === 'prevent' || m === 'stop').sort().join(',');
1975
- return `${binding.stateName}::${binding.statePathName}::${modifierKey}`;
2284
+ function getHandlerKey$3(binding, eventName) {
2285
+ const filterKey = binding.inFilters.map(f => f.filterName + '(' + f.args.join(',') + ')').join('|');
2286
+ return `${binding.stateName}::${binding.statePathName}::${eventName}::${filterKey}`;
1976
2287
  }
1977
- const stateEventHandlerFunction = (stateName, handlerName, modifiers, statePathInfo) => (event) => {
1978
- if (modifiers.includes('prevent'))
1979
- event.preventDefault();
1980
- if (modifiers.includes('stop'))
1981
- event.stopPropagation();
2288
+ function getEventName$2(binding) {
2289
+ let eventName = 'input';
2290
+ for (const modifier of binding.propModifiers) {
2291
+ if (modifier.startsWith('on')) {
2292
+ eventName = modifier.slice(2);
2293
+ }
2294
+ }
2295
+ return eventName;
2296
+ }
2297
+ const checkboxEventHandlerFunction = (stateName, statePathName, inFilters) => (event) => {
1982
2298
  const node = event.target;
2299
+ if (node === null) {
2300
+ console.warn(`[@wcstack/state] event.target is null.`);
2301
+ return;
2302
+ }
2303
+ if (node.type !== 'checkbox') {
2304
+ console.warn(`[@wcstack/state] event.target is not a checkbox input element.`);
2305
+ return;
2306
+ }
2307
+ const checked = node.checked;
2308
+ const newValue = node.value;
2309
+ let filteredNewValue = newValue;
2310
+ for (const filter of inFilters) {
2311
+ filteredNewValue = filter.filterFn(filteredNewValue);
2312
+ }
1983
2313
  const rootNode = node.getRootNode();
1984
2314
  const stateElement = getStateElementByName(rootNode, stateName);
1985
2315
  if (stateElement === null) {
1986
- raiseError(`State element with name "${stateName}" not found for event handler.`);
2316
+ raiseError(`State element with name "${stateName}" not found for two-way binding.`);
1987
2317
  }
1988
2318
  const loopContext = getLoopContextByNode(node);
1989
- const isCommand = isCommandTokenPath(handlerName);
1990
- stateElement.createStateAsync("writable", async (state) => {
2319
+ stateElement.createState("writable", (state) => {
1991
2320
  state[setLoopContextSymbol](loopContext, () => {
1992
- const indexes = loopContext?.listIndex.indexes ?? [];
1993
- if (isCommand) {
1994
- // command token を解決して emit。引数はハンドラ呼び出しと同じく (event, ...listIndexes) を透過する。
1995
- const token = state[getByAddressSymbol](createStateAddress(statePathInfo, null));
1996
- if (!isCommandToken(token)) {
1997
- raiseError(`Event binding "${handlerName}" did not resolve to a CommandToken. Declare the name in $commandTokens and reference it as $command.<name>.`);
2321
+ let currentValue = state[statePathName];
2322
+ if (Array.isArray(currentValue)) {
2323
+ if (checked) {
2324
+ if (currentValue.indexOf(filteredNewValue) === -1) {
2325
+ state[statePathName] = currentValue.concat(filteredNewValue);
2326
+ }
2327
+ }
2328
+ else {
2329
+ const index = currentValue.indexOf(filteredNewValue);
2330
+ if (index !== -1) {
2331
+ state[statePathName] = currentValue.toSpliced(index, 1);
2332
+ }
1998
2333
  }
1999
- return token.emit(event, ...indexes);
2000
2334
  }
2001
- const handler = state[handlerName];
2002
- if (typeof handler !== "function") {
2003
- raiseError(`Handler "${handlerName}" is not a function on state "${stateName}".`);
2335
+ else {
2336
+ if (checked) {
2337
+ state[statePathName] = [filteredNewValue];
2338
+ }
2339
+ else {
2340
+ state[statePathName] = [];
2341
+ }
2004
2342
  }
2005
- return Reflect.apply(handler, state, [event, ...indexes]);
2006
2343
  });
2007
2344
  });
2008
2345
  };
2009
- function attachEventHandler(binding) {
2010
- if (!binding.propName.startsWith("on")) {
2011
- return false;
2346
+ function attachCheckboxEventHandler(binding) {
2347
+ if (binding.bindingType === "checkbox" && binding.propModifiers.indexOf('ro') === -1) {
2348
+ const eventName = getEventName$2(binding);
2349
+ const key = getHandlerKey$3(binding, eventName);
2350
+ let checkboxEventHandler = handlerByHandlerKey$3.get(key);
2351
+ if (typeof checkboxEventHandler === "undefined") {
2352
+ checkboxEventHandler = checkboxEventHandlerFunction(binding.stateName, binding.statePathName, binding.inFilters);
2353
+ handlerByHandlerKey$3.set(key, checkboxEventHandler);
2354
+ }
2355
+ binding.node.addEventListener(eventName, checkboxEventHandler);
2356
+ bindingRegistry$3.add(key, binding);
2357
+ return true;
2358
+ }
2359
+ return false;
2360
+ }
2361
+ function detachCheckboxEventHandler(binding) {
2362
+ if (binding.bindingType === "checkbox" && binding.propModifiers.indexOf('ro') === -1) {
2363
+ const eventName = getEventName$2(binding);
2364
+ const key = getHandlerKey$3(binding, eventName);
2365
+ const checkboxEventHandler = handlerByHandlerKey$3.get(key);
2366
+ if (typeof checkboxEventHandler === "undefined") {
2367
+ return false;
2368
+ }
2369
+ binding.node.removeEventListener(eventName, checkboxEventHandler);
2370
+ if (bindingRegistry$3.countOf(key) === 0) {
2371
+ return false;
2372
+ }
2373
+ if (bindingRegistry$3.remove(key, binding)) {
2374
+ handlerByHandlerKey$3.delete(key);
2375
+ }
2376
+ return true;
2377
+ }
2378
+ return false;
2379
+ }
2380
+
2381
+ // command-token / event-token が共有する pub/sub プリミティブ。
2382
+ // _subscribers は Set のため挿入順を保持する。
2383
+ // emit() は subscribe() された順に呼び出され、戻り値配列も同じ順序で返る。
2384
+ //
2385
+ // 「誰が subscribe し誰が emit するか」だけが command / event の違い:
2386
+ // - command-token: element が subscribe / state が emit
2387
+ // - event-token: state(`$on`) が subscribe / element(listener) が emit
2388
+ class Token {
2389
+ _name;
2390
+ _subscribers = new Set();
2391
+ constructor(name) {
2392
+ this._name = name;
2012
2393
  }
2013
- const key = getHandlerKey$3(binding);
2014
- let stateEventHandler = handlerByHandlerKey$3.get(key);
2015
- if (typeof stateEventHandler === "undefined") {
2016
- stateEventHandler = stateEventHandlerFunction(binding.stateName, binding.statePathName, binding.propModifiers, binding.statePathInfo);
2017
- handlerByHandlerKey$3.set(key, stateEventHandler);
2394
+ get name() {
2395
+ return this._name;
2396
+ }
2397
+ get size() {
2398
+ return this._subscribers.size;
2399
+ }
2400
+ subscribe(fn) {
2401
+ this._subscribers.add(fn);
2402
+ return () => {
2403
+ this._subscribers.delete(fn);
2404
+ };
2405
+ }
2406
+ unsubscribe(fn) {
2407
+ return this._subscribers.delete(fn);
2408
+ }
2409
+ emit(...args) {
2410
+ const results = [];
2411
+ for (const fn of this._subscribers) {
2412
+ results.push(fn(...args));
2413
+ }
2414
+ return results;
2018
2415
  }
2019
- const eventName = binding.propName.slice(2);
2020
- binding.node.addEventListener(eventName, stateEventHandler);
2021
- bindingRegistry$3.add(key, binding);
2022
- return true;
2023
2416
  }
2024
2417
 
2025
2418
  // EventToken は共有 pub/sub プリミティブ Token の薄い特化(element→state 方向)。
@@ -2098,12 +2491,7 @@ function getWcBindable$1(element) {
2098
2491
  return null;
2099
2492
  }
2100
2493
  // attach 側で未定義要素は whenDefined 後に再試行するため、ここに来る時点で customClass は定義済み。
2101
- const customClass = customElements.get(customTagName);
2102
- const bindable = customClass?.wcBindable;
2103
- if (bindable?.protocol === "wc-bindable" && bindable?.version === 1) {
2104
- return bindable;
2105
- }
2106
- return null;
2494
+ return readBindableDeclaration(element);
2107
2495
  }
2108
2496
  function attachEventTokenHandler(binding) {
2109
2497
  if (binding.propSegments[0] !== "eventToken") {
@@ -2112,10 +2500,11 @@ function attachEventTokenHandler(binding) {
2112
2500
  const element = binding.node;
2113
2501
  // カスタム要素が未定義なら定義後に再試行(wcBindable が必要なため)。
2114
2502
  const customTagName = getCustomElement(element);
2115
- if (customTagName !== null && customElements.get(customTagName) === undefined) {
2116
- customElements.whenDefined(customTagName).then(() => {
2117
- attachEventTokenHandler(binding);
2118
- });
2503
+ const registry = getCustomElementRegistry();
2504
+ if (customTagName !== null && registry?.get(customTagName) === undefined) {
2505
+ if (registry === null) {
2506
+ raiseError(`CustomElementRegistry is unavailable for <${customTagName}>.`);
2507
+ }
2119
2508
  return true;
2120
2509
  }
2121
2510
  // 再評価で二重 attach しない。
@@ -2130,7 +2519,7 @@ function attachEventTokenHandler(binding) {
2130
2519
  if (bindable === null) {
2131
2520
  raiseError(`eventToken binding requires a wc-bindable custom element. <${element.tagName.toLowerCase()}> is not wc-bindable.`);
2132
2521
  }
2133
- const propDesc = bindable.properties.find((p) => p.name === propertyName);
2522
+ const propDesc = bindable.knownProperties.get(propertyName);
2134
2523
  if (typeof propDesc === "undefined") {
2135
2524
  raiseError(`Property "${propertyName}" is not declared in wcBindable.properties of <${element.tagName.toLowerCase()}>.`);
2136
2525
  }
@@ -2165,6 +2554,210 @@ function attachEventTokenHandler(binding) {
2165
2554
  listenerByBinding.set(binding, { eventName, handler });
2166
2555
  return true;
2167
2556
  }
2557
+ function detachEventTokenHandler(binding) {
2558
+ if (binding.propSegments[0] !== "eventToken") {
2559
+ return false;
2560
+ }
2561
+ const listener = listenerByBinding.get(binding);
2562
+ if (typeof listener === "undefined") {
2563
+ return false;
2564
+ }
2565
+ binding.node.removeEventListener(listener.eventName, listener.handler);
2566
+ listenerByBinding.delete(binding);
2567
+ return true;
2568
+ }
2569
+
2570
+ // CommandToken は共有 pub/sub プリミティブ Token の薄い特化。
2571
+ // instanceof による型判別を成立させるため独立クラスとして維持する。
2572
+ //
2573
+ // ownerStateName は devtools 計装(protocol §4.5)のための内部 optional 引数。
2574
+ // command-token-protocol の外部仕様は不変更(registry が渡すだけで、
2575
+ // subscribe/emit の意味論には一切影響しない)。
2576
+ class CommandToken extends Token {
2577
+ _ownerStateName;
2578
+ constructor(name, ownerStateName) {
2579
+ super(name);
2580
+ this._ownerStateName = ownerStateName ?? null;
2581
+ }
2582
+ emit(...args) {
2583
+ if (devtoolsSink !== null) {
2584
+ // subscriberCount 0 の emit(空撃ち)もそのまま流す — whenDefined 前の
2585
+ // command 空撃ちレース類をタイムラインで可視化するため
2586
+ devtoolsSink({
2587
+ type: "state:token-emit",
2588
+ kind: "command",
2589
+ stateName: this._ownerStateName,
2590
+ tokenName: this.name,
2591
+ args,
2592
+ subscriberCount: this.size,
2593
+ });
2594
+ }
2595
+ return super.emit(...args);
2596
+ }
2597
+ }
2598
+ function isCommandToken(value) {
2599
+ return value instanceof CommandToken;
2600
+ }
2601
+
2602
+ // onclick: $command.<name> のように、DOM イベントから command token を直接 emit する形式かを判定する。
2603
+ // 右辺が $command 名前空間配下のパス($command.<token>)のときに true。
2604
+ function isCommandTokenPath(statePathName) {
2605
+ return statePathName.startsWith(STATE_COMMAND_NAMESPACE_NAME + ".");
2606
+ }
2607
+ const handlerByHandlerKey$2 = new Map();
2608
+ // binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
2609
+ const bindingRegistry$2 = createHandlerBindingRegistry();
2610
+ function getHandlerKey$2(binding) {
2611
+ const modifierKey = binding.propModifiers.filter(m => m === 'prevent' || m === 'stop').sort().join(',');
2612
+ return `${binding.stateName}::${binding.statePathName}::${modifierKey}`;
2613
+ }
2614
+ const stateEventHandlerFunction = (stateName, handlerName, modifiers, statePathInfo) => (event) => {
2615
+ if (modifiers.includes('prevent'))
2616
+ event.preventDefault();
2617
+ if (modifiers.includes('stop'))
2618
+ event.stopPropagation();
2619
+ const node = event.target;
2620
+ const rootNode = node.getRootNode();
2621
+ const stateElement = getStateElementByName(rootNode, stateName);
2622
+ if (stateElement === null) {
2623
+ raiseError(`State element with name "${stateName}" not found for event handler.`);
2624
+ }
2625
+ const loopContext = getLoopContextByNode(node);
2626
+ const isCommand = isCommandTokenPath(handlerName);
2627
+ stateElement.createStateAsync("writable", async (state) => {
2628
+ state[setLoopContextSymbol](loopContext, () => {
2629
+ const indexes = loopContext?.listIndex.indexes ?? [];
2630
+ if (isCommand) {
2631
+ // command token を解決して emit。引数はハンドラ呼び出しと同じく (event, ...listIndexes) を透過する。
2632
+ const token = state[getByAddressSymbol](createStateAddress(statePathInfo, null));
2633
+ if (!isCommandToken(token)) {
2634
+ raiseError(`Event binding "${handlerName}" did not resolve to a CommandToken. Declare the name in $commandTokens and reference it as $command.<name>.`);
2635
+ }
2636
+ return token.emit(event, ...indexes);
2637
+ }
2638
+ const handler = state[handlerName];
2639
+ if (typeof handler !== "function") {
2640
+ raiseError(`Handler "${handlerName}" is not a function on state "${stateName}".`);
2641
+ }
2642
+ return Reflect.apply(handler, state, [event, ...indexes]);
2643
+ });
2644
+ });
2645
+ };
2646
+ function attachEventHandler(binding) {
2647
+ if (!binding.propName.startsWith("on")) {
2648
+ return false;
2649
+ }
2650
+ const key = getHandlerKey$2(binding);
2651
+ let stateEventHandler = handlerByHandlerKey$2.get(key);
2652
+ if (typeof stateEventHandler === "undefined") {
2653
+ stateEventHandler = stateEventHandlerFunction(binding.stateName, binding.statePathName, binding.propModifiers, binding.statePathInfo);
2654
+ handlerByHandlerKey$2.set(key, stateEventHandler);
2655
+ }
2656
+ const eventName = binding.propName.slice(2);
2657
+ binding.node.addEventListener(eventName, stateEventHandler);
2658
+ bindingRegistry$2.add(key, binding);
2659
+ return true;
2660
+ }
2661
+ function detachEventHandler(binding) {
2662
+ if (!binding.propName.startsWith("on")) {
2663
+ return false;
2664
+ }
2665
+ const key = getHandlerKey$2(binding);
2666
+ const stateEventHandler = handlerByHandlerKey$2.get(key);
2667
+ if (typeof stateEventHandler === "undefined") {
2668
+ return false;
2669
+ }
2670
+ const eventName = binding.propName.slice(2);
2671
+ binding.node.removeEventListener(eventName, stateEventHandler);
2672
+ if (bindingRegistry$2.countOf(key) === 0) {
2673
+ return false;
2674
+ }
2675
+ if (bindingRegistry$2.remove(key, binding)) {
2676
+ handlerByHandlerKey$2.delete(key);
2677
+ }
2678
+ return true;
2679
+ }
2680
+
2681
+ const handlerByHandlerKey$1 = new Map();
2682
+ // binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
2683
+ const bindingRegistry$1 = createHandlerBindingRegistry();
2684
+ function getHandlerKey$1(binding, eventName) {
2685
+ const filterKey = binding.inFilters.map(f => f.filterName + '(' + f.args.join(',') + ')').join('|');
2686
+ return `${binding.stateName}::${binding.statePathName}::${eventName}::${filterKey}`;
2687
+ }
2688
+ function getEventName$1(binding) {
2689
+ let eventName = 'input';
2690
+ for (const modifier of binding.propModifiers) {
2691
+ if (modifier.startsWith('on')) {
2692
+ eventName = modifier.slice(2);
2693
+ }
2694
+ }
2695
+ return eventName;
2696
+ }
2697
+ const radioEventHandlerFunction = (stateName, statePathName, inFilters) => (event) => {
2698
+ const node = event.target;
2699
+ if (node === null) {
2700
+ console.warn(`[@wcstack/state] event.target is null.`);
2701
+ return;
2702
+ }
2703
+ if (node.type !== 'radio') {
2704
+ console.warn(`[@wcstack/state] event.target is not a radio input element.`);
2705
+ return;
2706
+ }
2707
+ if (node.checked === false) {
2708
+ return;
2709
+ }
2710
+ const newValue = node.value;
2711
+ let filteredNewValue = newValue;
2712
+ for (const filter of inFilters) {
2713
+ filteredNewValue = filter.filterFn(filteredNewValue);
2714
+ }
2715
+ const rootNode = node.getRootNode();
2716
+ const stateElement = getStateElementByName(rootNode, stateName);
2717
+ if (stateElement === null) {
2718
+ raiseError(`State element with name "${stateName}" not found for two-way binding.`);
2719
+ }
2720
+ const loopContext = getLoopContextByNode(node);
2721
+ stateElement.createState("writable", (state) => {
2722
+ state[setLoopContextSymbol](loopContext, () => {
2723
+ state[statePathName] = filteredNewValue;
2724
+ });
2725
+ });
2726
+ };
2727
+ function attachRadioEventHandler(binding) {
2728
+ if (binding.bindingType === "radio" && binding.propModifiers.indexOf('ro') === -1) {
2729
+ const eventName = getEventName$1(binding);
2730
+ const key = getHandlerKey$1(binding, eventName);
2731
+ let radioEventHandler = handlerByHandlerKey$1.get(key);
2732
+ if (typeof radioEventHandler === "undefined") {
2733
+ radioEventHandler = radioEventHandlerFunction(binding.stateName, binding.statePathName, binding.inFilters);
2734
+ handlerByHandlerKey$1.set(key, radioEventHandler);
2735
+ }
2736
+ binding.node.addEventListener(eventName, radioEventHandler);
2737
+ bindingRegistry$1.add(key, binding);
2738
+ return true;
2739
+ }
2740
+ return false;
2741
+ }
2742
+ function detachRadioEventHandler(binding) {
2743
+ if (binding.bindingType === "radio" && binding.propModifiers.indexOf('ro') === -1) {
2744
+ const eventName = getEventName$1(binding);
2745
+ const key = getHandlerKey$1(binding, eventName);
2746
+ const radioEventHandler = handlerByHandlerKey$1.get(key);
2747
+ if (typeof radioEventHandler === "undefined") {
2748
+ return false;
2749
+ }
2750
+ binding.node.removeEventListener(eventName, radioEventHandler);
2751
+ if (bindingRegistry$1.countOf(key) === 0) {
2752
+ return false;
2753
+ }
2754
+ if (bindingRegistry$1.remove(key, binding)) {
2755
+ handlerByHandlerKey$1.delete(key);
2756
+ }
2757
+ return true;
2758
+ }
2759
+ return false;
2760
+ }
2168
2761
 
2169
2762
  const CHECK_TYPES = new Set(['radio', 'checkbox']);
2170
2763
  const DEFAULT_VALUE_PROP_NAMES = new Set(['value', 'valueAsNumber', 'valueAsDate']);
@@ -2192,47 +2785,159 @@ function isPossibleTwoWay(node, propName) {
2192
2785
  if (tagName === 'textarea' && propName === 'value') {
2193
2786
  return true;
2194
2787
  }
2195
- const customTagName = getCustomElement(element);
2196
- if (customTagName !== null) {
2197
- const customClass = customElements.get(customTagName);
2198
- if (typeof customClass === "undefined") {
2199
- raiseError(`Custom element <${customTagName}> is not defined. Cannot determine if property "${propName}" is suitable for two-way binding.`);
2200
- }
2201
- const bindable = customClass.wcBindable;
2202
- if (bindable?.protocol === "wc-bindable" && bindable?.version === 1) {
2203
- if (bindable.properties.some(p => p.name === propName)) {
2204
- return true;
2205
- }
2788
+ const customTagName = getCustomElement(element);
2789
+ if (customTagName !== null) {
2790
+ const customClass = getCustomElementRegistry()?.get(customTagName);
2791
+ if (typeof customClass === "undefined") {
2792
+ raiseError(`Custom element <${customTagName}> is not defined. Cannot determine if property "${propName}" is suitable for two-way binding.`);
2793
+ }
2794
+ const bindable = readBindableDeclaration(element);
2795
+ if (bindable?.knownProperties.has(propName)) {
2796
+ return true;
2797
+ }
2798
+ }
2799
+ return false;
2800
+ }
2801
+
2802
+ /**
2803
+ * propagation/propagation.ts
2804
+ *
2805
+ * Phase 3 の因果伝播コア(feature flag `enablePropagationContext` 下)。
2806
+ * 依存は types のみの葉モジュールとし、twowayHandler / applyChangeToProperty /
2807
+ * setByAddress / updater の計装点から循環 import なしで参照できるようにする。
2808
+ *
2809
+ * wire 識別は (node × member × stateName × statePathName) で行う。設計書の
2810
+ * WriteReceipt は bindingId + generation を持つが、twoway handler は共有
2811
+ * handler(handlerByHandlerKey)で binding インスタンスに到達できないため、
2812
+ * runtime の edge / receipt 照合キーは wire 単位とする。BindingSession
2813
+ * generation との統合(再 attach 後の edge ID 非再利用)は session 側の
2814
+ * 計装が揃う段階で bindingGeneration に反映する。
2815
+ */
2816
+ let nextWireId = 1;
2817
+ let nextTransactionId = 1;
2818
+ let nextSynchronousScopeId = 1;
2819
+ // node を強参照しない wire 台帳。inner key = `${stateName}::${statePathName}::${member}`
2820
+ const wireIdsByNode = new WeakMap();
2821
+ function wireKey(member, stateName, statePathName) {
2822
+ return `${stateName}::${statePathName}::${member}`;
2823
+ }
2824
+ /**
2825
+ * wire(配線)の安定 ID を返す。edge ID の基底と receipt の bindingId に使う。
2826
+ */
2827
+ function getWireId(node, member, stateName, statePathName) {
2828
+ let byKey = wireIdsByNode.get(node);
2829
+ if (typeof byKey === "undefined") {
2830
+ byKey = new Map();
2831
+ wireIdsByNode.set(node, byKey);
2832
+ }
2833
+ const key = wireKey(member, stateName, statePathName);
2834
+ let wireId = byKey.get(key);
2835
+ if (typeof wireId === "undefined") {
2836
+ wireId = nextWireId++;
2837
+ byKey.set(key, wireId);
2838
+ }
2839
+ return wireId;
2840
+ }
2841
+ /** wire × 方向 → edge ID。方向を含めるため再利用されない */
2842
+ function getEdgeId(wireId, direction) {
2843
+ return direction === "to-element" ? wireId * 2 : wireId * 2 + 1;
2844
+ }
2845
+ const EMPTY_EDGES = new Set();
2846
+ /** 外部 event / API update ごとの transaction 開始。current context は変更しない */
2847
+ function beginPropagationTransaction(originBindingId) {
2848
+ return {
2849
+ transactionId: nextTransactionId++,
2850
+ originBindingId,
2851
+ visitedEdges: EMPTY_EDGES,
2852
+ hop: 0,
2853
+ };
2854
+ }
2855
+ /** edge を 1 つ通過した新しい context を返す(visitedEdges 追加・hop+1) */
2856
+ function extendPropagationContext(context, edgeId) {
2857
+ const visitedEdges = new Set(context.visitedEdges);
2858
+ visitedEdges.add(edgeId);
2859
+ return {
2860
+ transactionId: context.transactionId,
2861
+ originBindingId: context.originBindingId,
2862
+ visitedEdges,
2863
+ hop: context.hop + 1,
2864
+ };
2865
+ }
2866
+ // 同期 dynamic scope の current context(updater drain / element 書き込み中に設定)
2867
+ let currentContext = null;
2868
+ function getCurrentPropagationContext() {
2869
+ return currentContext;
2870
+ }
2871
+ function runWithPropagationContext(context, callback) {
2872
+ const previous = currentContext;
2873
+ currentContext = context;
2874
+ try {
2875
+ return callback();
2876
+ }
2877
+ finally {
2878
+ currentContext = previous;
2879
+ }
2880
+ }
2881
+ const receiptStack = [];
2882
+ /**
2883
+ * state → element 書き込みを receipt scope で包んで実行する。
2884
+ * setter が同期 dispatch する event は matchWriteReceipt でこの receipt を観測できる。
2885
+ */
2886
+ function runWithWriteReceipt(node, member, writtenValue, bindingId, transactionId, callback) {
2887
+ const receipt = {
2888
+ bindingId,
2889
+ bindingGeneration: 0,
2890
+ member,
2891
+ transactionId,
2892
+ synchronousScopeId: nextSynchronousScopeId++,
2893
+ writtenValue,
2894
+ };
2895
+ receiptStack.push({ receipt, node });
2896
+ try {
2897
+ return callback();
2898
+ }
2899
+ finally {
2900
+ receiptStack.pop();
2901
+ }
2902
+ }
2903
+ /**
2904
+ * (node, member) に対する最も内側の active receipt を返す。
2905
+ * confirmation / normalization の判定(writtenValue との Object.is 比較)は
2906
+ * 呼び出し側が行う。scope 外(非同期に届いた event)では null。
2907
+ */
2908
+ function matchWriteReceipt(node, member) {
2909
+ for (let i = receiptStack.length - 1; i >= 0; i--) {
2910
+ const active = receiptStack[i];
2911
+ if (active.node === node && active.receipt.member === member) {
2912
+ return active.receipt;
2206
2913
  }
2207
2914
  }
2208
- return false;
2915
+ return null;
2209
2916
  }
2210
2917
 
2211
- const handlerByHandlerKey$2 = new Map();
2918
+ const handlerByHandlerKey = new Map();
2212
2919
  // binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
2213
- const bindingRegistry$2 = createHandlerBindingRegistry();
2920
+ const bindingRegistry = createHandlerBindingRegistry();
2921
+ const producerValueObserversByNode = new WeakMap();
2214
2922
  const DEFAULT_GETTER = (e) => e.detail;
2215
- function getHandlerKey$2(binding, eventName, hasGetter) {
2923
+ function getHandlerKey(binding, eventName, hasGetter) {
2216
2924
  const filterKey = binding.inFilters.map(f => f.filterName + '(' + f.args.join(',') + ')').join('|');
2217
2925
  return `${binding.stateName}::${binding.propName}::${binding.statePathName}::${eventName}::${filterKey}::${hasGetter ? 'g' : 'n'}`;
2218
2926
  }
2219
- function getEventName$2(binding) {
2927
+ function getEventName(binding) {
2220
2928
  const tagName = binding.node.tagName.toLowerCase();
2221
2929
  // 1.default event name
2222
2930
  let eventName = (tagName === 'select') ? 'change' : 'input';
2223
2931
  // 2.wcBindable protocol
2224
2932
  const customTagName = getCustomElement(binding.node);
2225
2933
  if (customTagName !== null) {
2226
- const customClass = customElements.get(customTagName);
2934
+ const customClass = getCustomElementRegistry()?.get(customTagName);
2227
2935
  if (typeof customClass === "undefined") {
2228
2936
  raiseError(`Custom element <${customTagName}> is not defined. Cannot determine event name for two-way binding.`);
2229
2937
  }
2230
- const bindable = customClass.wcBindable;
2231
- if (bindable?.protocol === "wc-bindable" && bindable?.version === 1) {
2232
- const propDesc = bindable.properties.find(p => p.name === binding.propName);
2233
- if (propDesc) {
2234
- eventName = propDesc.event;
2235
- }
2938
+ const propDesc = readBindableDeclaration(binding.node)?.knownProperties.get(binding.propName);
2939
+ if (propDesc) {
2940
+ eventName = propDesc.event;
2236
2941
  }
2237
2942
  }
2238
2943
  // 3.modifier
@@ -2246,15 +2951,9 @@ function getEventName$2(binding) {
2246
2951
  function getValueGetter(binding) {
2247
2952
  const customTagName = getCustomElement(binding.node);
2248
2953
  if (customTagName !== null) {
2249
- const customClass = customElements.get(customTagName);
2250
- if (customClass) {
2251
- const bindable = customClass.wcBindable;
2252
- if (bindable?.protocol === "wc-bindable" && bindable?.version === 1) {
2253
- const propDesc = bindable.properties.find(p => p.name === binding.propName);
2254
- if (propDesc) {
2255
- return propDesc.getter ?? DEFAULT_GETTER;
2256
- }
2257
- }
2954
+ const propDesc = readBindableDeclaration(binding.node)?.knownProperties.get(binding.propName);
2955
+ if (propDesc) {
2956
+ return propDesc.getter ?? DEFAULT_GETTER;
2258
2957
  }
2259
2958
  }
2260
2959
  return null;
@@ -2280,246 +2979,892 @@ const twowayEventHandlerFunction = (stateName, propName, statePathName, inFilter
2280
2979
  for (const filter of inFilters) {
2281
2980
  filteredNewValue = filter.filterFn(filteredNewValue);
2282
2981
  }
2982
+ const producerObservers = producerValueObserversByNode.get(node)?.get(propName);
2983
+ if (typeof producerObservers !== "undefined") {
2984
+ for (const observer of producerObservers)
2985
+ observer(filteredNewValue);
2986
+ }
2987
+ let propagationContext = null;
2988
+ if (config.enablePropagationContext) {
2989
+ // Phase 3: element → state edge の因果判定(設計書 §4)。
2990
+ const wireId = getWireId(node, propName, stateName, statePathName);
2991
+ const receipt = matchWriteReceipt(node, propName);
2992
+ if (receipt !== null && Object.is(receipt.writtenValue, newValue)) {
2993
+ // 規則 4: 同じ setter call stack 内で同じ member から Object.is 同値の
2994
+ // 通知が戻った場合だけ confirmation として再伝播を抑止する。
2995
+ // shadow diagnostic(§8): primitive なら same-value guard も同じ結論に
2996
+ // なるため、provenance だけが守っている非 primitive の echo を可視化する。
2997
+ if (config.debug) {
2998
+ console.debug(`[@wcstack/state] propagation: write confirmation suppressed echo.`, {
2999
+ node,
3000
+ propName,
3001
+ statePathName,
3002
+ transactionId: receipt.transactionId,
3003
+ coveredBySameValueGuard: config.sameValueGuard
3004
+ && (filteredNewValue === null || typeof filteredNewValue !== "object"),
3005
+ });
3006
+ }
3007
+ if (devtoolsSink !== null) {
3008
+ devtoolsSink({
3009
+ type: "propagation:suppressed",
3010
+ reason: "confirmation",
3011
+ transactionId: receipt.transactionId,
3012
+ edgeId: getEdgeId(wireId, "to-state"),
3013
+ node,
3014
+ member: propName,
3015
+ });
3016
+ }
3017
+ return;
3018
+ }
3019
+ // receipt があるが値が異なる場合は正規化差分: element の確定値として受理し、
3020
+ // 新しい edge を通る変更として継続する(規則 5・decision gate)。
3021
+ const toStateEdgeId = getEdgeId(wireId, "to-state");
3022
+ const baseContext = getCurrentPropagationContext();
3023
+ if (baseContext !== null && baseContext.visitedEdges.has(toStateEdgeId)) {
3024
+ // 規則 2: 同じ transaction が同じ edge を再度通ろうとした場合だけ抑止
3025
+ if (devtoolsSink !== null) {
3026
+ devtoolsSink({
3027
+ type: "propagation:suppressed",
3028
+ reason: "visited-edge",
3029
+ transactionId: baseContext.transactionId,
3030
+ edgeId: toStateEdgeId,
3031
+ node,
3032
+ member: propName,
3033
+ });
3034
+ }
3035
+ return;
3036
+ }
3037
+ // 規則 1: 外部 event(受け皿の context が無い)なら新しい transaction を開始
3038
+ propagationContext = extendPropagationContext(baseContext ?? beginPropagationTransaction(wireId), toStateEdgeId);
3039
+ }
2283
3040
  const rootNode = node.getRootNode();
2284
3041
  const stateElement = getStateElementByName(rootNode, stateName);
2285
3042
  if (stateElement === null) {
2286
3043
  raiseError(`State element with name "${stateName}" not found for two-way binding.`);
2287
3044
  }
2288
3045
  const loopContext = getLoopContextByNode(node);
2289
- stateElement.createState("writable", (state) => {
2290
- state[setLoopContextSymbol](loopContext, () => {
2291
- state[statePathName] = filteredNewValue;
3046
+ const commitToState = () => {
3047
+ stateElement.createState("writable", (state) => {
3048
+ state[setLoopContextSymbol](loopContext, () => {
3049
+ state[statePathName] = filteredNewValue;
3050
+ });
2292
3051
  });
2293
- });
3052
+ };
3053
+ if (propagationContext !== null) {
3054
+ runWithPropagationContext(propagationContext, commitToState);
3055
+ }
3056
+ else {
3057
+ commitToState();
3058
+ }
2294
3059
  };
3060
+ function addTwowayValueObserver(node, propName, observer) {
3061
+ let byProperty = producerValueObserversByNode.get(node);
3062
+ if (typeof byProperty === "undefined") {
3063
+ byProperty = new Map();
3064
+ producerValueObserversByNode.set(node, byProperty);
3065
+ }
3066
+ let observers = byProperty.get(propName);
3067
+ if (typeof observers === "undefined") {
3068
+ observers = new Set();
3069
+ byProperty.set(propName, observers);
3070
+ }
3071
+ observers.add(observer);
3072
+ return () => {
3073
+ observers?.delete(observer);
3074
+ if (observers?.size === 0)
3075
+ byProperty?.delete(propName);
3076
+ if (byProperty?.size === 0)
3077
+ producerValueObserversByNode.delete(node);
3078
+ };
3079
+ }
2295
3080
  function attachTwowayEventHandler(binding) {
2296
3081
  const customTagName = getCustomElement(binding.node);
2297
3082
  if (customTagName !== null) {
2298
- const customClass = customElements.get(customTagName);
3083
+ const registry = getCustomElementRegistry();
3084
+ const customClass = registry?.get(customTagName);
2299
3085
  if (typeof customClass === "undefined") {
2300
- customElements.whenDefined(customTagName).then(() => {
2301
- attachTwowayEventHandler(binding);
2302
- });
3086
+ if (registry === null) {
3087
+ raiseError(`CustomElementRegistry is unavailable for <${customTagName}>.`);
3088
+ }
2303
3089
  return;
2304
3090
  }
2305
3091
  }
2306
3092
  if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf('ro') === -1) {
2307
- const eventName = getEventName$2(binding);
3093
+ const eventName = getEventName(binding);
2308
3094
  const valueGetter = getValueGetter(binding);
2309
- const key = getHandlerKey$2(binding, eventName, valueGetter !== null);
2310
- let twowayEventHandler = handlerByHandlerKey$2.get(key);
3095
+ const key = getHandlerKey(binding, eventName, valueGetter !== null);
3096
+ let twowayEventHandler = handlerByHandlerKey.get(key);
2311
3097
  if (typeof twowayEventHandler === "undefined") {
2312
3098
  twowayEventHandler = twowayEventHandlerFunction(binding.stateName, binding.propName, binding.statePathName, binding.inFilters, valueGetter);
2313
- handlerByHandlerKey$2.set(key, twowayEventHandler);
3099
+ handlerByHandlerKey.set(key, twowayEventHandler);
2314
3100
  }
2315
3101
  binding.node.addEventListener(eventName, twowayEventHandler);
2316
- bindingRegistry$2.add(key, binding);
3102
+ bindingRegistry.add(key, binding);
2317
3103
  }
2318
3104
  }
2319
-
2320
- const lastListValueByAbsoluteStateAddress = new WeakMap();
2321
- function getLastListValueByAbsoluteStateAddress(address) {
2322
- return lastListValueByAbsoluteStateAddress.get(address) ?? [];
2323
- }
2324
- function setLastListValueByAbsoluteStateAddress(address, value) {
2325
- lastListValueByAbsoluteStateAddress.set(address, value);
2326
- }
2327
-
2328
- const _cache$2 = new WeakMap();
2329
- function getAbsolutePathInfo(stateElement, pathInfo) {
2330
- if (_cache$2.has(stateElement)) {
2331
- const pathMap = _cache$2.get(stateElement);
2332
- if (pathMap.has(pathInfo)) {
2333
- return pathMap.get(pathInfo);
3105
+ function detachTwowayEventHandler(binding) {
3106
+ const customTagName = getCustomElement(binding.node);
3107
+ if (customTagName !== null) {
3108
+ const registry = getCustomElementRegistry();
3109
+ const customClass = registry?.get(customTagName);
3110
+ if (typeof customClass === "undefined") {
3111
+ if (registry === null) {
3112
+ return;
3113
+ }
3114
+ return;
2334
3115
  }
2335
3116
  }
2336
- else {
2337
- _cache$2.set(stateElement, new WeakMap());
2338
- }
2339
- const absolutePathInfo = Object.freeze(new AbsolutePathInfo(stateElement, pathInfo));
2340
- _cache$2.get(stateElement).set(pathInfo, absolutePathInfo);
2341
- return absolutePathInfo;
2342
- }
2343
- class AbsolutePathInfo {
2344
- pathInfo;
2345
- stateName;
2346
- stateElement;
2347
- parentAbsolutePathInfo;
2348
- constructor(stateElement, pathInfo) {
2349
- this.pathInfo = pathInfo;
2350
- this.stateName = stateElement.name;
2351
- this.stateElement = stateElement;
2352
- if (pathInfo.parentPathInfo === null) {
2353
- this.parentAbsolutePathInfo = null;
3117
+ if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf('ro') === -1) {
3118
+ const eventName = getEventName(binding);
3119
+ const valueGetter = getValueGetter(binding);
3120
+ const key = getHandlerKey(binding, eventName, valueGetter !== null);
3121
+ const twowayEventHandler = handlerByHandlerKey.get(key);
3122
+ if (typeof twowayEventHandler === "undefined") {
3123
+ return;
2354
3124
  }
2355
- else {
2356
- this.parentAbsolutePathInfo = getAbsolutePathInfo(stateElement, pathInfo.parentPathInfo);
3125
+ binding.node.removeEventListener(eventName, twowayEventHandler);
3126
+ if (bindingRegistry.remove(key, binding)) {
3127
+ handlerByHandlerKey.delete(key);
2357
3128
  }
2358
3129
  }
2359
3130
  }
2360
3131
 
2361
- const _cache$1 = new WeakMap();
2362
- const _cacheNullListIndex = new WeakMap();
2363
- class AbsoluteStateAddress {
2364
- absolutePathInfo;
2365
- listIndex;
2366
- _parentAbsoluteAddress;
2367
- constructor(absolutePathInfo, listIndex) {
2368
- this.absolutePathInfo = absolutePathInfo;
2369
- this.listIndex = listIndex;
3132
+ /**
3133
+ * Shares one CustomElementRegistry.whenDefined() continuation per registry/tag.
3134
+ * Waiters can be removed independently, so a never-defined tag does not retain
3135
+ * binding records or their DOM nodes after teardown.
3136
+ */
3137
+ class DefinitionCoordinator {
3138
+ registry;
3139
+ entries = new Map();
3140
+ constructor(registry) {
3141
+ this.registry = registry;
2370
3142
  }
2371
- get parentAbsoluteAddress() {
2372
- if (typeof this._parentAbsoluteAddress !== 'undefined') {
2373
- return this._parentAbsoluteAddress;
3143
+ wait(tagName, resolve, reject = () => undefined) {
3144
+ const normalizedTagName = tagName.toLowerCase();
3145
+ let entry = this.entries.get(normalizedTagName);
3146
+ if (typeof entry === "undefined") {
3147
+ entry = { waiters: new Set() };
3148
+ this.entries.set(normalizedTagName, entry);
3149
+ this.registry.whenDefined(normalizedTagName).then(() => this.settle(normalizedTagName, null), (error) => this.settle(normalizedTagName, error));
2374
3150
  }
2375
- const parentAbsolutePathInfo = this.absolutePathInfo.parentAbsolutePathInfo;
2376
- if (parentAbsolutePathInfo === null) {
2377
- return null;
3151
+ const waiter = { active: true, resolve, reject };
3152
+ entry.waiters.add(waiter);
3153
+ return () => {
3154
+ if (!waiter.active)
3155
+ return;
3156
+ waiter.active = false;
3157
+ entry?.waiters.delete(waiter);
3158
+ };
3159
+ }
3160
+ pendingCount(tagName) {
3161
+ return this.entries.get(tagName.toLowerCase())?.waiters.size ?? 0;
3162
+ }
3163
+ settle(tagName, error) {
3164
+ const entry = this.entries.get(tagName);
3165
+ if (typeof entry === "undefined")
3166
+ return;
3167
+ this.entries.delete(tagName);
3168
+ const waiters = Array.from(entry.waiters);
3169
+ entry.waiters.clear();
3170
+ for (const waiter of waiters) {
3171
+ if (!waiter.active)
3172
+ continue;
3173
+ waiter.active = false;
3174
+ if (error === null)
3175
+ waiter.resolve();
3176
+ else
3177
+ waiter.reject(error);
2378
3178
  }
2379
- const lastSegment = this.absolutePathInfo.pathInfo.segments[this.absolutePathInfo.pathInfo.segments.length - 1];
2380
- let parentListIndex = null;
2381
- if (lastSegment === WILDCARD) {
2382
- parentListIndex = this.listIndex?.parentListIndex ?? null;
3179
+ }
3180
+ }
3181
+ const coordinatorByRegistry = new WeakMap();
3182
+ function getDefinitionCoordinator(registry) {
3183
+ let coordinator = coordinatorByRegistry.get(registry);
3184
+ if (typeof coordinator === "undefined") {
3185
+ coordinator = new DefinitionCoordinator(registry);
3186
+ coordinatorByRegistry.set(registry, coordinator);
3187
+ }
3188
+ return coordinator;
3189
+ }
3190
+
3191
+ function readOption(binding, key) {
3192
+ let result = null;
3193
+ for (const modifier of binding.propModifiers) {
3194
+ const separator = modifier.indexOf("=");
3195
+ if (separator < 0)
3196
+ continue;
3197
+ const modifierKey = modifier.slice(0, separator).trim();
3198
+ const value = modifier.slice(separator + 1).trim();
3199
+ if (modifierKey !== "init" && modifierKey !== "sync") {
3200
+ raiseError(`Unknown binding modifier "${modifierKey}" in "${modifier}".`);
2383
3201
  }
2384
- else {
2385
- parentListIndex = this.listIndex;
3202
+ if (modifierKey !== key)
3203
+ continue;
3204
+ if (result !== null) {
3205
+ raiseError(`Binding modifier "${key}" may only be specified once.`);
2386
3206
  }
2387
- return this._parentAbsoluteAddress = createAbsoluteStateAddress(parentAbsolutePathInfo, parentListIndex);
3207
+ result = value;
2388
3208
  }
3209
+ return result;
2389
3210
  }
2390
- function createAbsoluteStateAddress(absolutePathInfo, listIndex) {
2391
- if (listIndex === null) {
2392
- let cached = _cacheNullListIndex.get(absolutePathInfo);
2393
- if (typeof cached !== "undefined") {
2394
- return cached;
2395
- }
2396
- cached = new AbsoluteStateAddress(absolutePathInfo, null);
2397
- _cacheNullListIndex.set(absolutePathInfo, cached);
2398
- return cached;
3211
+ function parseAuthority(value) {
3212
+ if (value === null)
3213
+ return null;
3214
+ if (value === "state" || value === "element" || value === "auto" || value === "none") {
3215
+ return value;
2399
3216
  }
2400
- else {
2401
- let cacheByAbsolutePathInfo = _cache$1.get(listIndex);
2402
- if (typeof cacheByAbsolutePathInfo === "undefined") {
2403
- cacheByAbsolutePathInfo = new WeakMap();
2404
- _cache$1.set(listIndex, cacheByAbsolutePathInfo);
2405
- }
2406
- let cached = cacheByAbsolutePathInfo.get(absolutePathInfo);
2407
- if (typeof cached !== "undefined") {
2408
- return cached;
3217
+ return raiseError(`Invalid init modifier value "${value}".`);
3218
+ }
3219
+ function parseSyncOn(value) {
3220
+ if (value === null || value === "call")
3221
+ return "call";
3222
+ if (value === "connect")
3223
+ return "connect";
3224
+ return raiseError(`Invalid sync modifier value "${value}".`);
3225
+ }
3226
+ function hasInitialSyncModifier(binding) {
3227
+ return binding.propModifiers.some((modifier) => modifier.includes("="));
3228
+ }
3229
+ function resolveInitialSyncPolicy(binding) {
3230
+ if (!config.enableDirectionalInitialSync) {
3231
+ if (hasInitialSyncModifier(binding)) {
3232
+ raiseError("init=/sync= modifiers require enableDirectionalInitialSync.");
2409
3233
  }
2410
- cached = new AbsoluteStateAddress(absolutePathInfo, listIndex);
2411
- cacheByAbsolutePathInfo.set(absolutePathInfo, cached);
2412
- return cached;
3234
+ return { authority: "state", syncOn: "call", observable: false };
3235
+ }
3236
+ const explicitAuthority = parseAuthority(readOption(binding, "init"));
3237
+ const syncOn = parseSyncOn(readOption(binding, "sync"));
3238
+ if (binding.bindingType === "event") {
3239
+ if (explicitAuthority !== null && explicitAuthority !== "none") {
3240
+ raiseError("Event bindings only allow init=none.");
3241
+ }
3242
+ return { authority: "none", syncOn, observable: false };
3243
+ }
3244
+ // command.<name>: $command.<method> は命令的な command-token 配線。bindingType は
3245
+ // "prop" だが propName ("command.<name>") は wcBindable property ではないため、下の
3246
+ // property authority 検証(未宣言なら raiseError)に掛けてはならない。値の初期同期を
3247
+ // 持たない配線なので、現行互換の "state" authority を返す(command token は従来通り
3248
+ // 初期 apply で配線される)。
3249
+ if (binding.propSegments[0] === "command") {
3250
+ return { authority: "state", syncOn, observable: false };
3251
+ }
3252
+ if (binding.bindingType !== "prop") {
3253
+ if (explicitAuthority !== null && explicitAuthority !== "state" && explicitAuthority !== "none") {
3254
+ raiseError(`Binding type "${binding.bindingType}" does not support init=${explicitAuthority}.`);
3255
+ }
3256
+ return { authority: explicitAuthority ?? "state", syncOn, observable: false };
3257
+ }
3258
+ const declaration = readBindableDeclaration(binding.node);
3259
+ if (declaration === null) {
3260
+ return { authority: explicitAuthority ?? "state", syncOn, observable: false };
3261
+ }
3262
+ const hasOutput = declaration.knownProperties.has(binding.propName);
3263
+ const hasInput = declaration.declaredInputs.has(binding.propName);
3264
+ if (!hasOutput && !hasInput) {
3265
+ raiseError(`Property "${binding.propName}" is not declared by wcBindable.`);
3266
+ }
3267
+ const allowed = hasOutput && hasInput
3268
+ ? new Set(["state", "element", "auto", "none"])
3269
+ : hasOutput
3270
+ ? new Set(["element", "none"])
3271
+ : new Set(["state", "none"]);
3272
+ const defaultAuthority = hasOutput && !hasInput ? "element" : "state";
3273
+ const authority = explicitAuthority ?? defaultAuthority;
3274
+ if (!allowed.has(authority)) {
3275
+ raiseError(`init=${authority} is incompatible with wcBindable member "${binding.propName}".`);
3276
+ }
3277
+ if (syncOn === "connect" && !hasOutput) {
3278
+ raiseError(`sync=connect requires observable property "${binding.propName}".`);
3279
+ }
3280
+ return { authority, syncOn, observable: hasOutput };
3281
+ }
3282
+ function isBindingStateInitialized(binding) {
3283
+ const rootNode = binding.replaceNode.getRootNode();
3284
+ const stateElement = getStateElementByName(rootNode, binding.stateName);
3285
+ if (stateElement === null) {
3286
+ raiseError(`State element with name "${binding.stateName}" not found for binding.`);
2413
3287
  }
3288
+ const address = getStateAddressByBindingInfo(binding);
3289
+ let initialized = false;
3290
+ stateElement.createState("readonly", (state) => {
3291
+ initialized = state[hasByAddressSymbol](address);
3292
+ });
3293
+ return initialized;
2414
3294
  }
2415
-
2416
- const rootNodeByFragment = new WeakMap();
2417
- function setRootNodeByFragment(fragment, rootNode) {
2418
- if (rootNode === null) {
2419
- rootNodeByFragment.delete(fragment);
3295
+ function resolveInitialAuthority(binding, authority) {
3296
+ if (authority !== "auto")
3297
+ return authority;
3298
+ return isBindingStateInitialized(binding) ? "state" : "element";
3299
+ }
3300
+ function commitProducerValue(binding, value) {
3301
+ let filteredValue = value;
3302
+ for (const filter of binding.inFilters) {
3303
+ filteredValue = filter.filterFn(filteredValue);
2420
3304
  }
2421
- else {
2422
- rootNodeByFragment.set(fragment, rootNode);
3305
+ const rootNode = binding.node.getRootNode();
3306
+ const stateElement = getStateElementByName(rootNode, binding.stateName);
3307
+ if (stateElement === null) {
3308
+ raiseError(`State element with name "${binding.stateName}" not found for initial binding sync.`);
2423
3309
  }
3310
+ const loopContext = getLoopContextByNode(binding.node);
3311
+ stateElement.createState("writable", (state) => {
3312
+ state[setLoopContextSymbol](loopContext, () => {
3313
+ state[binding.statePathName] = filteredValue;
3314
+ });
3315
+ });
2424
3316
  }
2425
- function getRootNodeByFragment(fragment) {
2426
- return rootNodeByFragment.get(fragment) || null;
3317
+
3318
+ function replaceToReplaceNode(bindingInfo) {
3319
+ const node = bindingInfo.node;
3320
+ const replaceNode = bindingInfo.replaceNode;
3321
+ if (node === replaceNode) {
3322
+ return;
3323
+ }
3324
+ if (node.parentNode === null) {
3325
+ // already replaced
3326
+ return;
3327
+ }
3328
+ node.parentNode.replaceChild(replaceNode, node);
2427
3329
  }
2428
3330
 
2429
- const cacheCalcWildcardLen = new Map();
2430
- function calcWildcardLen(pathInfo, targetPathInfo) {
2431
- let path1;
2432
- let path2;
2433
- if (pathInfo.wildcardCount === 0 || targetPathInfo.wildcardCount === 0) {
2434
- return 0;
3331
+ let nextRecordId = 0;
3332
+ let nextGeneration = 0;
3333
+ const recordByBinding = new WeakMap();
3334
+ const sessionByRoot = new WeakMap();
3335
+ function forEachInclusive(root, callback) {
3336
+ callback(root);
3337
+ for (const child of Array.from(root.childNodes)) {
3338
+ forEachInclusive(child, callback);
2435
3339
  }
2436
- if (pathInfo.wildcardCount === 1
2437
- && targetPathInfo.wildcardCount > 0
2438
- && targetPathInfo.wildcardPathSet.has(pathInfo.path)) {
2439
- return 1;
3340
+ }
3341
+ function isObservableRoot(value) {
3342
+ if (typeof value !== "object" || value === null)
3343
+ return false;
3344
+ const node = value;
3345
+ return node.nodeType === 9 || (node.nodeType === 11 && "host" in node);
3346
+ }
3347
+ function observableRootFor(node) {
3348
+ const root = node.getRootNode();
3349
+ return isObservableRoot(root) ? root : null;
3350
+ }
3351
+ class BindingOwner {
3352
+ root;
3353
+ sessionRefs = new Set();
3354
+ knownSessions = new WeakSet();
3355
+ observer;
3356
+ constructor(root) {
3357
+ this.root = root;
3358
+ const Observer = globalThis.MutationObserver;
3359
+ this.observer = typeof Observer === "function"
3360
+ ? new Observer((mutations) => this.handleMutations(mutations))
3361
+ : null;
3362
+ this.observer?.observe(root, { childList: true, subtree: true });
3363
+ }
3364
+ add(session) {
3365
+ if (this.knownSessions.has(session))
3366
+ return;
3367
+ this.knownSessions.add(session);
3368
+ this.sessionRefs.add(new WeakRef(session));
3369
+ }
3370
+ handleMutations(mutations) {
3371
+ const removed = [];
3372
+ const added = [];
3373
+ for (const mutation of mutations) {
3374
+ removed.push(...Array.from(mutation.removedNodes));
3375
+ added.push(...Array.from(mutation.addedNodes));
3376
+ }
3377
+ for (const ref of Array.from(this.sessionRefs)) {
3378
+ const session = ref.deref();
3379
+ if (typeof session === "undefined") {
3380
+ this.sessionRefs.delete(ref);
3381
+ continue;
3382
+ }
3383
+ session.handleMutations(this.root, removed, added);
3384
+ }
3385
+ }
3386
+ }
3387
+ const ownerByRoot = new WeakMap();
3388
+ function getBindingOwner(root) {
3389
+ let owner = ownerByRoot.get(root);
3390
+ if (typeof owner === "undefined") {
3391
+ owner = new BindingOwner(root);
3392
+ ownerByRoot.set(root, owner);
3393
+ }
3394
+ return owner;
3395
+ }
3396
+ function bindingKey(binding) {
3397
+ const inFilters = binding.inFilters.map((filter) => `${filter.filterName}(${filter.args.join(",")})`).join("|");
3398
+ const outFilters = binding.outFilters.map((filter) => `${filter.filterName}(${filter.args.join(",")})`).join("|");
3399
+ return [
3400
+ binding.bindingType,
3401
+ binding.propName,
3402
+ binding.propModifiers.join(","),
3403
+ binding.stateName,
3404
+ binding.statePathName,
3405
+ inFilters,
3406
+ outFilters,
3407
+ binding.uuid ?? "",
3408
+ ].join("\u0000");
3409
+ }
3410
+ class BindingSession {
3411
+ records = new Set();
3412
+ knownBindingsByNode = new WeakMap();
3413
+ optionsByBinding = new WeakMap();
3414
+ deferredByNode = new WeakMap();
3415
+ deferred = new Set();
3416
+ constructor(root = null) {
3417
+ if (root !== null)
3418
+ this.observe(root);
3419
+ }
3420
+ initialize(bindings, options = {}) {
3421
+ const registerAddress = options.registerAddress ?? true;
3422
+ const resolvedOptions = {
3423
+ registerAddress,
3424
+ registerPathInfo: options.registerPathInfo ?? registerAddress,
3425
+ applyOnReconnect: options.applyOnReconnect ?? true,
3426
+ };
3427
+ const initialized = [];
3428
+ for (const candidate of bindings) {
3429
+ const binding = this.remember(candidate, resolvedOptions);
3430
+ const existing = recordByBinding.get(binding);
3431
+ if (typeof existing !== "undefined" && existing.phase !== "disposed" && existing.phase !== "failed") {
3432
+ this.observe(existing.anchor);
3433
+ if (resolvedOptions.registerAddress && existing.address === null) {
3434
+ existing.options.registerAddress = true;
3435
+ this.registerAddress(existing);
3436
+ }
3437
+ if (existing.phase === "active")
3438
+ this.settleInitialRecord(existing);
3439
+ this.settleConnectedSnapshot(existing);
3440
+ continue;
3441
+ }
3442
+ this.start(binding, resolvedOptions);
3443
+ initialized.push(binding);
3444
+ }
3445
+ return initialized.filter((binding) => this.shouldApplyState(binding));
3446
+ }
3447
+ shouldApplyState(binding) {
3448
+ if (!config.enableDirectionalInitialSync) {
3449
+ if (hasInitialSyncModifier(binding))
3450
+ resolveInitialSyncPolicy(binding);
3451
+ return true;
3452
+ }
3453
+ const record = recordByBinding.get(binding);
3454
+ if (typeof record === "undefined" || record.session !== this)
3455
+ return true;
3456
+ if (!record.options.registerAddress || record.phase === "waiting-definition")
3457
+ return true;
3458
+ if (record.phase === "active")
3459
+ this.settleInitialRecord(record);
3460
+ return record.resolvedAuthority === "state";
3461
+ }
3462
+ getRecord(binding) {
3463
+ const record = recordByBinding.get(binding);
3464
+ return record?.session === this ? record : null;
3465
+ }
3466
+ addTeardown(binding, teardown) {
3467
+ const record = recordByBinding.get(binding);
3468
+ if (typeof record === "undefined" || !this.isAlive(record, record.generation)) {
3469
+ return false;
3470
+ }
3471
+ record.teardowns.add(teardown);
3472
+ return true;
3473
+ }
3474
+ deferUntilDefined(node, tagName, callback, reject = () => undefined) {
3475
+ const registry = getCustomElementRegistry();
3476
+ if (registry === null) {
3477
+ raiseError(`CustomElementRegistry is unavailable for <${tagName}>.`);
3478
+ }
3479
+ this.observe(node);
3480
+ const task = { node, active: true, cancel: null };
3481
+ let tasks = this.deferredByNode.get(node);
3482
+ if (typeof tasks === "undefined") {
3483
+ tasks = new Set();
3484
+ this.deferredByNode.set(node, tasks);
3485
+ }
3486
+ tasks.add(task);
3487
+ this.deferred.add(task);
3488
+ const finish = () => {
3489
+ if (!task.active)
3490
+ return false;
3491
+ task.active = false;
3492
+ tasks?.delete(task);
3493
+ this.deferred.delete(task);
3494
+ return true;
3495
+ };
3496
+ task.cancel = getDefinitionCoordinator(registry).wait(tagName, () => {
3497
+ if (!finish())
3498
+ return;
3499
+ try {
3500
+ upgradeCustomElement(registry, node);
3501
+ callback();
3502
+ }
3503
+ catch (error) {
3504
+ reject(error);
3505
+ }
3506
+ }, (error) => {
3507
+ if (!finish())
3508
+ return;
3509
+ reject(error);
3510
+ });
3511
+ return () => {
3512
+ if (!finish())
3513
+ return;
3514
+ task.cancel?.();
3515
+ };
3516
+ }
3517
+ disposeBinding(binding) {
3518
+ const record = recordByBinding.get(binding);
3519
+ if (typeof record === "undefined" || record.session !== this)
3520
+ return;
3521
+ this.disposeRecord(record);
3522
+ }
3523
+ dispose() {
3524
+ for (const record of Array.from(this.records))
3525
+ this.disposeRecord(record);
3526
+ for (const task of Array.from(this.deferred)) {
3527
+ task.active = false;
3528
+ task.cancel?.();
3529
+ this.deferred.delete(task);
3530
+ this.deferredByNode.get(task.node)?.delete(task);
3531
+ }
3532
+ }
3533
+ observe(node) {
3534
+ const root = observableRootFor(node);
3535
+ if (root === null)
3536
+ return;
3537
+ getBindingOwner(root).add(this);
3538
+ }
3539
+ handleMutations(root, removed, added) {
3540
+ for (const subtree of removed) {
3541
+ forEachInclusive(subtree, (node) => {
3542
+ if (root.contains(node))
3543
+ return;
3544
+ const known = this.knownBindingsByNode.get(node);
3545
+ if (typeof known !== "undefined") {
3546
+ for (const binding of known.values())
3547
+ this.disposeBinding(binding);
3548
+ }
3549
+ const tasks = this.deferredByNode.get(node);
3550
+ if (typeof tasks !== "undefined") {
3551
+ for (const task of Array.from(tasks)) {
3552
+ task.active = false;
3553
+ task.cancel?.();
3554
+ tasks.delete(task);
3555
+ this.deferred.delete(task);
3556
+ }
3557
+ }
3558
+ });
3559
+ }
3560
+ const reconnected = [];
3561
+ for (const subtree of added) {
3562
+ forEachInclusive(subtree, (node) => {
3563
+ if (!root.contains(node))
3564
+ return;
3565
+ const known = this.knownBindingsByNode.get(node);
3566
+ if (typeof known === "undefined")
3567
+ return;
3568
+ for (const binding of known.values()) {
3569
+ const record = recordByBinding.get(binding);
3570
+ if (record?.phase === "active") {
3571
+ this.settleConnectedSnapshot(record);
3572
+ continue;
3573
+ }
3574
+ if (record?.phase !== "disposed")
3575
+ continue;
3576
+ const options = this.optionsByBinding.get(binding);
3577
+ if (typeof options === "undefined")
3578
+ continue;
3579
+ try {
3580
+ this.start(binding, options);
3581
+ if (options.applyOnReconnect && this.shouldApplyState(binding))
3582
+ reconnected.push(binding);
3583
+ }
3584
+ catch {
3585
+ // Mutation delivery cannot surface initialization errors to a caller.
3586
+ }
3587
+ }
3588
+ });
3589
+ }
3590
+ if (reconnected.length > 0)
3591
+ applyChangeFromBindings(reconnected);
2440
3592
  }
2441
- if (pathInfo.id < targetPathInfo.id) {
2442
- path1 = pathInfo;
2443
- path2 = targetPathInfo;
3593
+ remember(binding, options) {
3594
+ const anchor = binding.replaceNode;
3595
+ let known = this.knownBindingsByNode.get(anchor);
3596
+ if (typeof known === "undefined") {
3597
+ known = new Map();
3598
+ this.knownBindingsByNode.set(anchor, known);
3599
+ }
3600
+ const key = bindingKey(binding);
3601
+ const remembered = known.get(key);
3602
+ if (typeof remembered !== "undefined") {
3603
+ const rememberedOptions = this.optionsByBinding.get(remembered);
3604
+ if (typeof rememberedOptions !== "undefined") {
3605
+ rememberedOptions.registerAddress ||= options.registerAddress;
3606
+ rememberedOptions.registerPathInfo ||= options.registerPathInfo;
3607
+ rememberedOptions.applyOnReconnect ||= options.applyOnReconnect;
3608
+ }
3609
+ return remembered;
3610
+ }
3611
+ known.set(key, binding);
3612
+ this.optionsByBinding.set(binding, { ...options });
3613
+ return binding;
2444
3614
  }
2445
- else {
2446
- path1 = targetPathInfo;
2447
- path2 = pathInfo;
3615
+ start(binding, options) {
3616
+ replaceToReplaceNode(binding);
3617
+ const recordOptions = this.optionsByBinding.get(binding) ?? { ...options };
3618
+ const record = {
3619
+ id: ++nextRecordId,
3620
+ info: binding,
3621
+ generation: ++nextGeneration,
3622
+ phase: "discovered",
3623
+ teardowns: new Set(),
3624
+ session: this,
3625
+ anchor: binding.replaceNode,
3626
+ options: recordOptions,
3627
+ address: null,
3628
+ pendingDefinitions: 0,
3629
+ initialPolicy: null,
3630
+ resolvedAuthority: null,
3631
+ initialSettled: false,
3632
+ observationPending: false,
3633
+ eventSequence: 0,
3634
+ hasProducerValue: false,
3635
+ producerValue: undefined,
3636
+ };
3637
+ recordByBinding.set(binding, record);
3638
+ this.records.add(record);
3639
+ this.observe(record.anchor);
3640
+ try {
3641
+ record.phase = "attaching";
3642
+ this.attachListeners(record);
3643
+ if (record.options.registerAddress)
3644
+ this.registerAddress(record);
3645
+ if (record.pendingDefinitions === 0)
3646
+ record.phase = "active";
3647
+ }
3648
+ catch (error) {
3649
+ record.phase = "failed";
3650
+ this.runTeardowns(record);
3651
+ this.records.delete(record);
3652
+ throw error;
3653
+ }
2448
3654
  }
2449
- const key = `${path1.path}\t${path2.path}`;
2450
- let len = cacheCalcWildcardLen.get(key);
2451
- if (typeof len !== "undefined") {
2452
- return len;
3655
+ attachListeners(record) {
3656
+ const binding = record.info;
3657
+ if (attachEventHandler(binding)) {
3658
+ record.teardowns.add(() => detachEventHandler(binding));
3659
+ return;
3660
+ }
3661
+ if (binding.propSegments[0] === "eventToken") {
3662
+ this.attachAfterDefinition(record, () => {
3663
+ if (attachEventTokenHandler(binding)) {
3664
+ record.teardowns.add(() => detachEventTokenHandler(binding));
3665
+ }
3666
+ });
3667
+ return;
3668
+ }
3669
+ if (attachRadioEventHandler(binding)) {
3670
+ record.teardowns.add(() => detachRadioEventHandler(binding));
3671
+ }
3672
+ if (attachCheckboxEventHandler(binding)) {
3673
+ record.teardowns.add(() => detachCheckboxEventHandler(binding));
3674
+ }
3675
+ this.attachAfterDefinition(record, () => {
3676
+ // directional initial sync の producer-value observer は twowayEventHandlerFunction
3677
+ // からのみ呼ばれる(唯一の consumer)。その handler が attach されるのは
3678
+ // isPossibleTwoWay かつ非 ro の binding だけ(attachTwowayEventHandler と同条件)
3679
+ // なので、one-way / event / eventToken / radio(非value) 等では observer は決して
3680
+ // fire しない。以前は attachListeners 冒頭で全 binding に無条件登録していたが、
3681
+ // fire しえない大多数の binding に対する setup 死荷重だった。ここへ移すことで
3682
+ // 「twoway handler が付く binding のみ observer 登録」を構造的に保証する
3683
+ // (undefined custom element は attachAfterDefinition が定義後まで遅延するので
3684
+ // isPossibleTwoWay の未定義 CE raiseError も踏まない)。
3685
+ if (config.enableDirectionalInitialSync
3686
+ && isPossibleTwoWay(binding.node, binding.propName)
3687
+ && binding.propModifiers.indexOf("ro") === -1) {
3688
+ const removeObserver = addTwowayValueObserver(binding.node, binding.propName, (value) => {
3689
+ if (!this.isAlive(record, record.generation))
3690
+ return;
3691
+ record.eventSequence += 1;
3692
+ record.hasProducerValue = true;
3693
+ record.producerValue = value;
3694
+ });
3695
+ record.teardowns.add(removeObserver);
3696
+ }
3697
+ attachTwowayEventHandler(binding);
3698
+ record.teardowns.add(() => detachTwowayEventHandler(binding));
3699
+ });
2453
3700
  }
2454
- const matchPath = path1.wildcardPathSet.intersection(path2.wildcardPathSet);
2455
- len = matchPath.size;
2456
- cacheCalcWildcardLen.set(key, len);
2457
- return len;
2458
- }
2459
-
2460
- const listIndexByBindingInfoByLoopContext = new WeakMap();
2461
- function getListIndexByBindingInfo(bindingInfo) {
2462
- const loopContext = getLoopContextByNode(bindingInfo.node);
2463
- if (loopContext === null) {
2464
- return null;
3701
+ attachAfterDefinition(record, attach) {
3702
+ const tagName = getCustomElement(record.info.node);
3703
+ if (tagName === null) {
3704
+ attach();
3705
+ return;
3706
+ }
3707
+ const registry = getCustomElementRegistry();
3708
+ if (registry === null) {
3709
+ raiseError(`CustomElementRegistry is unavailable for <${tagName}>.`);
3710
+ }
3711
+ if (typeof registry.get(tagName) !== "undefined") {
3712
+ attach();
3713
+ return;
3714
+ }
3715
+ record.phase = "waiting-definition";
3716
+ record.pendingDefinitions += 1;
3717
+ const generation = record.generation;
3718
+ const coordinator = getDefinitionCoordinator(registry);
3719
+ const cancel = coordinator.wait(tagName, () => {
3720
+ if (!this.isAlive(record, generation))
3721
+ return;
3722
+ try {
3723
+ upgradeCustomElement(registry, record.info.node);
3724
+ attach();
3725
+ record.pendingDefinitions -= 1;
3726
+ if (record.pendingDefinitions === 0) {
3727
+ record.phase = "active";
3728
+ this.settleInitialRecord(record);
3729
+ }
3730
+ }
3731
+ catch {
3732
+ record.phase = "failed";
3733
+ this.runTeardowns(record);
3734
+ this.records.delete(record);
3735
+ }
3736
+ }, () => {
3737
+ if (!this.isAlive(record, generation))
3738
+ return;
3739
+ record.phase = "failed";
3740
+ this.runTeardowns(record);
3741
+ this.records.delete(record);
3742
+ });
3743
+ record.teardowns.add(cancel);
2465
3744
  }
2466
- let listIndexByBindingInfo = listIndexByBindingInfoByLoopContext.get(loopContext);
2467
- if (typeof listIndexByBindingInfo === "undefined") {
2468
- listIndexByBindingInfo = new WeakMap();
2469
- listIndexByBindingInfoByLoopContext.set(loopContext, listIndexByBindingInfo);
3745
+ settleInitialRecord(record) {
3746
+ if (!config.enableDirectionalInitialSync || record.initialSettled || !record.options.registerAddress)
3747
+ return;
3748
+ record.phase = "synchronizing";
3749
+ try {
3750
+ const policy = resolveInitialSyncPolicy(record.info);
3751
+ const authority = resolveInitialAuthority(record.info, policy.authority);
3752
+ record.initialPolicy = policy;
3753
+ record.resolvedAuthority = authority;
3754
+ record.initialSettled = true;
3755
+ record.phase = "active";
3756
+ if (!policy.observable)
3757
+ return;
3758
+ if (policy.syncOn === "connect"
3759
+ && record.info.node instanceof HTMLElement
3760
+ && !record.info.node.isConnected) {
3761
+ record.observationPending = true;
3762
+ return;
3763
+ }
3764
+ this.readProducerSnapshot(record, policy.syncOn === "call");
3765
+ }
3766
+ catch (error) {
3767
+ record.phase = "failed";
3768
+ this.runTeardowns(record);
3769
+ this.records.delete(record);
3770
+ throw error;
3771
+ }
2470
3772
  }
2471
- else {
2472
- const listIndex = listIndexByBindingInfo.get(bindingInfo);
2473
- if (typeof listIndex !== "undefined") {
2474
- return listIndex;
3773
+ readProducerSnapshot(record, eventWins) {
3774
+ if (!this.isAlive(record, record.generation))
3775
+ return;
3776
+ const target = record.info.node;
3777
+ const name = record.info.propName;
3778
+ if (!(name in target))
3779
+ return;
3780
+ const sequence = record.eventSequence;
3781
+ const value = target[name];
3782
+ record.observationPending = false;
3783
+ if (eventWins && record.eventSequence !== sequence)
3784
+ return;
3785
+ record.hasProducerValue = true;
3786
+ record.producerValue = value;
3787
+ if (record.resolvedAuthority === "element") {
3788
+ commitProducerValue(record.info, value);
2475
3789
  }
2476
3790
  }
2477
- let listIndex = null;
2478
- try {
2479
- const wildcardLen = calcWildcardLen(loopContext.pathInfo, bindingInfo.statePathInfo);
2480
- if (wildcardLen > 0) {
2481
- listIndex = loopContext.listIndex.at(wildcardLen - 1);
3791
+ settleConnectedSnapshot(record) {
3792
+ if (!config.enableDirectionalInitialSync
3793
+ || !record.observationPending
3794
+ || !(record.info.node instanceof HTMLElement)
3795
+ || !record.info.node.isConnected)
3796
+ return;
3797
+ try {
3798
+ this.readProducerSnapshot(record, false);
3799
+ }
3800
+ catch {
3801
+ record.phase = "failed";
3802
+ this.runTeardowns(record);
3803
+ this.records.delete(record);
2482
3804
  }
2483
- return listIndex;
2484
3805
  }
2485
- finally {
2486
- listIndexByBindingInfo.set(bindingInfo, listIndex);
3806
+ registerAddress(record) {
3807
+ if (record.address !== null)
3808
+ return;
3809
+ const binding = record.info;
3810
+ const address = getAbsoluteStateAddressByBinding(binding);
3811
+ addBindingByAbsoluteStateAddress(address, binding);
3812
+ record.address = address;
3813
+ record.teardowns.add(() => {
3814
+ if (record.address === null)
3815
+ return;
3816
+ removeBindingByAbsoluteStateAddress(record.address, binding);
3817
+ record.address = null;
3818
+ clearStateAddressByBindingInfo(binding);
3819
+ clearAbsoluteStateAddressByBinding(binding);
3820
+ });
3821
+ if (!record.options.registerPathInfo)
3822
+ return;
3823
+ const rootNode = binding.replaceNode.getRootNode();
3824
+ const stateElement = getStateElementByName(rootNode, binding.stateName);
3825
+ if (stateElement === null) {
3826
+ raiseError(`State element with name "${binding.stateName}" not found for binding.`);
3827
+ }
3828
+ if (binding.bindingType !== "event") {
3829
+ stateElement.setPathInfo(binding.statePathName, binding.bindingType);
3830
+ }
2487
3831
  }
2488
- }
2489
-
2490
- const absoluteStateAddressByBinding = new WeakMap();
2491
- function getAbsoluteStateAddressByBinding(binding) {
2492
- // 切断されていても、キャッシュされていれば絶対状態アドレスを返す。
2493
- let absoluteStateAddress = null;
2494
- absoluteStateAddress = absoluteStateAddressByBinding.get(binding) || null;
2495
- if (absoluteStateAddress !== null) {
2496
- return absoluteStateAddress;
3832
+ isAlive(record, generation) {
3833
+ return record.generation === generation
3834
+ && recordByBinding.get(record.info) === record
3835
+ && record.phase !== "disposed"
3836
+ && record.phase !== "failed";
2497
3837
  }
2498
- let rootNode = binding.replaceNode.getRootNode();
2499
- // binding.replaceNodeはisConnected=trueになっていることが前提、切断されている場合はraiseErrorを返す
2500
- if (binding.replaceNode.isConnected === false) {
2501
- // DocumentFragmentでバッファリングされている場合は、ルートノードをDocumentFragmentから実際のルートノードに切り替える
2502
- const rootNodeByFragment = getRootNodeByFragment(rootNode);
2503
- if (rootNodeByFragment === null) {
2504
- raiseError(`Cannot get absolute state address for disconnected binding: ${binding.bindingType} ${binding.statePathName} on ${binding.node.nodeName}`);
2505
- }
2506
- else {
2507
- rootNode = rootNodeByFragment;
3838
+ disposeRecord(record) {
3839
+ if (record.phase === "disposed")
3840
+ return;
3841
+ record.phase = "disposed";
3842
+ this.runTeardowns(record);
3843
+ this.records.delete(record);
3844
+ }
3845
+ runTeardowns(record) {
3846
+ const teardowns = Array.from(record.teardowns).reverse();
3847
+ record.teardowns.clear();
3848
+ for (const teardown of teardowns) {
3849
+ try {
3850
+ teardown();
3851
+ }
3852
+ catch {
3853
+ // Cleanup is best-effort; one faulty resource must not retain the rest.
3854
+ }
2508
3855
  }
2509
3856
  }
2510
- const listIndex = getListIndexByBindingInfo(binding);
2511
- const stateElement = getStateElementByName(rootNode, binding.stateName);
2512
- if (stateElement === null) {
2513
- raiseError(`State element with name "${binding.stateName}" not found for binding.`);
3857
+ }
3858
+ function getOrCreateBindingSession(root) {
3859
+ let session = sessionByRoot.get(root);
3860
+ if (typeof session === "undefined") {
3861
+ session = new BindingSession(root);
3862
+ sessionByRoot.set(root, session);
2514
3863
  }
2515
- const absolutePathInfo = getAbsolutePathInfo(stateElement, binding.statePathInfo);
2516
- absoluteStateAddress =
2517
- createAbsoluteStateAddress(absolutePathInfo, listIndex);
2518
- absoluteStateAddressByBinding.set(binding, absoluteStateAddress);
2519
- return absoluteStateAddress;
3864
+ return session;
2520
3865
  }
2521
- function clearAbsoluteStateAddressByBinding(binding) {
2522
- absoluteStateAddressByBinding.delete(binding);
3866
+ function getBindingSession(binding) {
3867
+ return recordByBinding.get(binding)?.session ?? null;
2523
3868
  }
2524
3869
 
2525
3870
  const completeByStateElementByWebComponent = new WeakMap();
@@ -2603,15 +3948,11 @@ function getWcBindable(element) {
2603
3948
  if (customTagName === null) {
2604
3949
  return null;
2605
3950
  }
2606
- const customClass = customElements.get(customTagName);
3951
+ const customClass = getCustomElementRegistry()?.get(customTagName);
2607
3952
  if (typeof customClass === "undefined") {
2608
3953
  raiseError(`Custom element <${customTagName}> is not defined for command binding.`);
2609
3954
  }
2610
- const bindable = customClass.wcBindable;
2611
- if (bindable?.protocol === "wc-bindable" && bindable?.version === 1) {
2612
- return bindable;
2613
- }
2614
- return null;
3955
+ return readBindableDeclaration(element);
2615
3956
  }
2616
3957
  function applyChangeToCommand(binding, _context, newValue) {
2617
3958
  if (!isCommandToken(newValue)) {
@@ -2632,7 +3973,7 @@ function applyChangeToCommand(binding, _context, newValue) {
2632
3973
  if (bindable === null) {
2633
3974
  raiseError(`command binding requires a wc-bindable custom element. <${element.tagName.toLowerCase()}> is not wc-bindable.`);
2634
3975
  }
2635
- if (!Array.isArray(bindable.commands) || !bindable.commands.some((c) => c.name === methodName)) {
3976
+ if (!bindable.declaredCommands.has(methodName)) {
2636
3977
  raiseError(`Command "${methodName}" is not declared in wcBindable.commands of <${element.tagName.toLowerCase()}>.`);
2637
3978
  }
2638
3979
  // ここまで来たら旧解除して新 subscribe に切り替える。
@@ -3149,42 +4490,6 @@ function computeStableIndexSet(diff) {
3149
4490
  return stable;
3150
4491
  }
3151
4492
 
3152
- const bindingSetByAbsoluteStateAddress = new WeakMap();
3153
- function getBindingSetByAbsoluteStateAddress(absoluteStateAddress) {
3154
- let bindingSet = null;
3155
- bindingSet = bindingSetByAbsoluteStateAddress.get(absoluteStateAddress) || null;
3156
- if (bindingSet === null) {
3157
- bindingSet = new Set();
3158
- bindingSetByAbsoluteStateAddress.set(absoluteStateAddress, bindingSet);
3159
- }
3160
- return bindingSet;
3161
- }
3162
- /**
3163
- * 参照専用の取得。get-or-create と違い、未登録アドレスに空 Set を
3164
- * 生成・キャッシュしない(リスト置換の drain は大量のバインディング無し
3165
- * アドレスを照会するため、生成すると空 Set が溜まり続ける)。
3166
- */
3167
- function peekBindingSetByAbsoluteStateAddress(absoluteStateAddress) {
3168
- return bindingSetByAbsoluteStateAddress.get(absoluteStateAddress);
3169
- }
3170
- function addBindingByAbsoluteStateAddress(absoluteStateAddress, binding) {
3171
- const bindingSet = getBindingSetByAbsoluteStateAddress(absoluteStateAddress);
3172
- bindingSet.add(binding);
3173
- if (devtoolsSink !== null) {
3174
- devtoolsSink({ type: "state:binding-added", absoluteAddress: absoluteStateAddress, binding });
3175
- }
3176
- }
3177
- function removeBindingByAbsoluteStateAddress(absoluteStateAddress, binding) {
3178
- // get-or-create を通すと未登録アドレスに空 Set を生成してしまうため素の get で参照する
3179
- const bindingSet = bindingSetByAbsoluteStateAddress.get(absoluteStateAddress);
3180
- if (bindingSet !== undefined) {
3181
- bindingSet.delete(binding);
3182
- if (devtoolsSink !== null) {
3183
- devtoolsSink({ type: "state:binding-removed", absoluteAddress: absoluteStateAddress, binding });
3184
- }
3185
- }
3186
- }
3187
-
3188
4493
  const bindingsByContent = new WeakMap();
3189
4494
  function getBindingsByContent(content) {
3190
4495
  return bindingsByContent.get(content) ?? [];
@@ -3193,6 +4498,14 @@ function setBindingsByContent(content, bindings) {
3193
4498
  bindingsByContent.set(content, bindings);
3194
4499
  }
3195
4500
 
4501
+ const bindingSessionByContent = new WeakMap();
4502
+ function getBindingSessionByContent(content) {
4503
+ return bindingSessionByContent.get(content) ?? null;
4504
+ }
4505
+ function setBindingSessionByContent(content, session) {
4506
+ bindingSessionByContent.set(content, session);
4507
+ }
4508
+
3196
4509
  const nodesByContent = new WeakMap();
3197
4510
  function getNodesByContent(content) {
3198
4511
  return nodesByContent.get(content) ?? [];
@@ -3217,9 +4530,22 @@ function unbindLoopContextToContent(content) {
3217
4530
  function activateContent(content, loopContext, context) {
3218
4531
  bindLoopContextToContent(content, loopContext);
3219
4532
  const bindings = getBindingsByContent(content);
4533
+ const session = getBindingSessionByContent(content);
4534
+ if (session !== null) {
4535
+ session.initialize(bindings, {
4536
+ registerAddress: true,
4537
+ registerPathInfo: false,
4538
+ applyOnReconnect: false,
4539
+ });
4540
+ }
3220
4541
  for (const binding of bindings) {
3221
- const absoluteStateAddress = getAbsoluteStateAddressByBinding(binding);
3222
- addBindingByAbsoluteStateAddress(absoluteStateAddress, binding);
4542
+ if (session === null) {
4543
+ const absoluteStateAddress = getAbsoluteStateAddressByBinding(binding);
4544
+ addBindingByAbsoluteStateAddress(absoluteStateAddress, binding);
4545
+ }
4546
+ if (session !== null && !session.shouldApplyState(binding)) {
4547
+ continue;
4548
+ }
3223
4549
  applyChange(binding, context);
3224
4550
  }
3225
4551
  }
@@ -3228,13 +4554,15 @@ function deactivateContent(content) {
3228
4554
  return;
3229
4555
  }
3230
4556
  const bindings = getBindingsByContent(content);
4557
+ const session = getBindingSessionByContent(content);
3231
4558
  for (const binding of bindings) {
3232
- const absoluteStateAddress = getAbsoluteStateAddressByBinding(binding);
3233
- removeBindingByAbsoluteStateAddress(absoluteStateAddress, binding);
3234
- // アドレスキャッシュ(absoluteStateAddressByBinding / stateAddressByBindingInfo)
3235
- // のクリアはここでは行わない。deactivateContent の呼び出し元(for/if)は必ず
3236
- // 直後に content.unmount() を呼び、unmount が同じ2台帳をネスト content も含めて
3237
- // クリアする(createContent.ts)。ここで消すと全 binding で二重 delete になる。
4559
+ if (session !== null) {
4560
+ session.disposeBinding(binding);
4561
+ }
4562
+ else {
4563
+ const absoluteStateAddress = getAbsoluteStateAddressByBinding(binding);
4564
+ removeBindingByAbsoluteStateAddress(absoluteStateAddress, binding);
4565
+ }
3238
4566
  }
3239
4567
  unbindLoopContextToContent(content);
3240
4568
  }
@@ -3271,31 +4599,6 @@ function deleteContentByNode(node, content) {
3271
4599
  }
3272
4600
  }
3273
4601
 
3274
- const stateAddressByBindingInfo = new WeakMap();
3275
- function getStateAddressByBindingInfo(bindingInfo) {
3276
- let stateAddress = null;
3277
- stateAddress = stateAddressByBindingInfo.get(bindingInfo) || null;
3278
- if (stateAddress !== null) {
3279
- return stateAddress;
3280
- }
3281
- if (bindingInfo.statePathInfo.wildcardCount > 0) {
3282
- const listIndex = getListIndexByBindingInfo(bindingInfo);
3283
- if (listIndex === null) {
3284
- raiseError(`Cannot resolve state address for binding with wildcard statePathName "${bindingInfo.statePathName}" because list index is null.`);
3285
- }
3286
- stateAddress = createStateAddress(bindingInfo.statePathInfo, listIndex);
3287
- }
3288
- else {
3289
- stateAddress = createStateAddress(bindingInfo.statePathInfo, null);
3290
- }
3291
- stateAddressByBindingInfo.set(bindingInfo, stateAddress);
3292
- return stateAddress;
3293
- }
3294
- // call for change loopContext
3295
- function clearStateAddressByBindingInfo(bindingInfo) {
3296
- stateAddressByBindingInfo.delete(bindingInfo);
3297
- }
3298
-
3299
4602
  const recursiveBindingTypes = new Set(['if', 'elseif', 'else', 'for']);
3300
4603
  class Content {
3301
4604
  _content;
@@ -3335,6 +4638,7 @@ class Content {
3335
4638
  this._mounted = true;
3336
4639
  }
3337
4640
  unmount() {
4641
+ getBindingSessionByContent(this)?.dispose();
3338
4642
  for (const node of this._childNodeArray) {
3339
4643
  if (node.parentNode !== null) {
3340
4644
  node.parentNode.removeChild(node);
@@ -3380,6 +4684,7 @@ function createContent(bindingInfo) {
3380
4684
  const cloneFragment = document.importNode(fragmentInfo.fragment, true);
3381
4685
  const initialInfo = initializeBindingsByFragment(cloneFragment, fragmentInfo.nodeInfos);
3382
4686
  const content = new Content(cloneFragment);
4687
+ setBindingSessionByContent(content, initialInfo.bindingSession);
3383
4688
  setBindingsByContent(content, initialInfo.bindingInfos);
3384
4689
  const indexBindings = [];
3385
4690
  for (const binding of initialInfo.bindingInfos) {
@@ -3686,22 +4991,13 @@ function getInputAttributeMirror(element, propName) {
3686
4991
  if (customTagName === null) {
3687
4992
  return null;
3688
4993
  }
3689
- const customClass = customElements.get(customTagName);
4994
+ const customClass = getCustomElementRegistry()?.get(customTagName);
3690
4995
  if (typeof customClass === "undefined") {
3691
4996
  return null;
3692
4997
  }
3693
- const bindable = customClass.wcBindable;
3694
- if (bindable?.protocol !== "wc-bindable" || bindable?.version !== 1) {
3695
- return null;
3696
- }
3697
- const inputs = bindable.inputs;
3698
- if (!Array.isArray(inputs)) {
3699
- return null;
3700
- }
3701
- for (const input of inputs) {
3702
- if (input.name === propName && typeof input.attribute === "string" && input.attribute.length > 0) {
3703
- return input.attribute;
3704
- }
4998
+ const input = readBindableDeclaration(element)?.declaredInputs.get(propName);
4999
+ if (typeof input?.attribute === "string" && input.attribute.length > 0) {
5000
+ return input.attribute;
3705
5001
  }
3706
5002
  return null;
3707
5003
  }
@@ -3828,40 +5124,82 @@ function applyChangeToProperty(binding, _context, newValue) {
3828
5124
  if (propSegments.length === 1) {
3829
5125
  const firstSegment = propSegments[0];
3830
5126
  if (element[firstSegment] !== newValue) {
3831
- let propertyWriteSucceeded = false;
3832
- try {
3833
- element[firstSegment] = newValue;
3834
- propertyWriteSucceeded = true;
3835
- }
3836
- catch (error) {
3837
- if (config.debug) {
3838
- console.warn(`Failed to set property '${firstSegment}' on element.`, {
3839
- element,
3840
- newValue,
3841
- error
3842
- });
5127
+ const performWrite = () => {
5128
+ let propertyWriteSucceeded = false;
5129
+ try {
5130
+ element[firstSegment] = newValue;
5131
+ propertyWriteSucceeded = true;
3843
5132
  }
3844
- }
3845
- // wc-bindable inputs[].attribute ミラー。プロパティ書き込みが成功したときだけ
3846
- // 属性へ反映する。setter が値を拒否した場合に属性だけ進んでしまうと
3847
- // property と attribute が乖離し、attributeChangedCallback や CSS セレクタが
3848
- // 実際のプロパティ値と矛盾した状態で発火するため、ここでガードする。
3849
- if (propertyWriteSucceeded) {
3850
- const mirrorAttr = getInputAttributeMirror(element, firstSegment);
3851
- if (mirrorAttr !== null) {
3852
- try {
3853
- applyMirrorAttribute(element, mirrorAttr, newValue);
5133
+ catch (error) {
5134
+ if (config.debug) {
5135
+ console.warn(`Failed to set property '${firstSegment}' on element.`, {
5136
+ element,
5137
+ newValue,
5138
+ error
5139
+ });
3854
5140
  }
3855
- catch (error) {
3856
- if (config.debug) {
3857
- console.warn(`Failed to mirror attribute '${mirrorAttr}' on element.`, {
3858
- element,
3859
- newValue,
3860
- error
3861
- });
5141
+ }
5142
+ // wc-bindable inputs[].attribute ミラー。プロパティ書き込みが成功したときだけ
5143
+ // 属性へ反映する。setter が値を拒否した場合に属性だけ進んでしまうと
5144
+ // property と attribute が乖離し、attributeChangedCallback や CSS セレクタが
5145
+ // 実際のプロパティ値と矛盾した状態で発火するため、ここでガードする。
5146
+ if (propertyWriteSucceeded) {
5147
+ const mirrorAttr = getInputAttributeMirror(element, firstSegment);
5148
+ if (mirrorAttr !== null) {
5149
+ try {
5150
+ applyMirrorAttribute(element, mirrorAttr, newValue);
5151
+ }
5152
+ catch (error) {
5153
+ if (config.debug) {
5154
+ console.warn(`Failed to mirror attribute '${mirrorAttr}' on element.`, {
5155
+ element,
5156
+ newValue,
5157
+ error
5158
+ });
5159
+ }
3862
5160
  }
3863
5161
  }
3864
5162
  }
5163
+ };
5164
+ // Zero-cost fast path (§4 最適化): the propagation edge / WriteReceipt
5165
+ // machinery only matters when the element write can *echo* — i.e. the setter
5166
+ // may synchronously dispatch an event a two-way wire feeds back to state.
5167
+ // `isPossibleTwoWay` is the same conservative check the two-way listener
5168
+ // registration uses, and it is cheap for the common one-way case (textContent
5169
+ // / class / style on plain elements return false fast). One-way bindings can
5170
+ // never re-traverse an edge, so skipping the context/receipt is safe and
5171
+ // avoids a per-apply Set copy + receipt allocation. Diamond / coalescing are
5172
+ // unaffected — those ride the write-transaction context threaded through the
5173
+ // updater, not the element edge.
5174
+ if (config.enablePropagationContext && isPossibleTwoWay(element, firstSegment)) {
5175
+ // Phase 3: state → element edge の通過を記録し、同じ transaction が
5176
+ // 同じ edge を再度通ろうとした場合だけ抑止する(設計書 §4 規則 2)。
5177
+ // 書き込みは WriteReceipt scope で包み、setter が同期 dispatch する
5178
+ // event が confirmation / 正規化を判定できるようにする(規則 3)。
5179
+ const wireId = getWireId(element, firstSegment, binding.stateName, binding.statePathName);
5180
+ const edgeId = getEdgeId(wireId, "to-element");
5181
+ const baseContext = _context?.propagationContextByBinding?.get(binding)
5182
+ ?? getCurrentPropagationContext()
5183
+ ?? beginPropagationTransaction(wireId);
5184
+ if (baseContext.visitedEdges.has(edgeId)) {
5185
+ if (devtoolsSink !== null) {
5186
+ devtoolsSink({
5187
+ type: "propagation:suppressed",
5188
+ reason: "visited-edge",
5189
+ transactionId: baseContext.transactionId,
5190
+ edgeId,
5191
+ node: element,
5192
+ member: firstSegment,
5193
+ });
5194
+ }
5195
+ }
5196
+ else {
5197
+ const extendedContext = extendPropagationContext(baseContext, edgeId);
5198
+ runWithPropagationContext(extendedContext, () => runWithWriteReceipt(element, firstSegment, newValue, wireId, extendedContext.transactionId, performWrite));
5199
+ }
5200
+ }
5201
+ else {
5202
+ performWrite();
3865
5203
  }
3866
5204
  }
3867
5205
  if (inSsr()) {
@@ -4003,35 +5341,52 @@ function getValue(state, binding) {
4003
5341
  }
4004
5342
  }
4005
5343
 
4006
- // applyChange が「未 define のカスタム要素」への適用を見送った binding の台帳。
4007
- // define されるまでの間、同じ binding に対して applyChange は(state 更新の
4008
- // たびに)何度も呼ばれうるため、whenDefined の多重登録をここで抑止する。
4009
- // WeakSet なので binding の寿命に追従し、恒久 define されないタグでもリークしない。
4010
5344
  const scheduledBindings = new WeakSet();
4011
- /**
4012
- * define のカスタム要素に対する適用を customElements.whenDefined 後に再実行
4013
- * する。two-way / event-token の attach、spread の deferred 展開はいずれも
4014
- * whenDefined で再試行するのに対し、値の適用だけが片道 skip だった非対称の解消
4015
- * (docs/state-binding-init-races.md §2)。
4016
- *
4017
- * 再適用は applyChangeFromBindings を通すため、define 時点の最新 state 値で
4018
- * 適用される(skip 時点の値を保持しない)。define を待つ間に DOM から外れた
4019
- * binding には適用しない(deferred spread と同じ規約)。
4020
- */
5345
+ function reportFailure(tagName, error) {
5346
+ console.error(`[@wcstack/state] deferred apply failed for <${tagName}>.`, error);
5347
+ }
4021
5348
  function scheduleDeferredApply(binding, tagName) {
4022
- if (scheduledBindings.has(binding)) {
5349
+ if (scheduledBindings.has(binding))
4023
5350
  return;
4024
- }
4025
5351
  scheduledBindings.add(binding);
4026
- customElements.whenDefined(tagName).then(() => {
5352
+ const applyLatest = () => {
5353
+ scheduledBindings.delete(binding);
5354
+ const currentSession = getBindingSession(binding);
5355
+ if (currentSession !== null && !currentSession.shouldApplyState(binding)) {
5356
+ return;
5357
+ }
5358
+ applyChangeFromBindings([binding]);
5359
+ };
5360
+ const reject = (error) => {
5361
+ scheduledBindings.delete(binding);
5362
+ reportFailure(tagName, error);
5363
+ };
5364
+ const session = getBindingSession(binding);
5365
+ if (session !== null) {
5366
+ const cancel = session.deferUntilDefined(binding.replaceNode, tagName, applyLatest, reject);
5367
+ if (!session.addTeardown(binding, () => {
5368
+ scheduledBindings.delete(binding);
5369
+ cancel();
5370
+ })) {
5371
+ scheduledBindings.delete(binding);
5372
+ cancel();
5373
+ }
5374
+ return;
5375
+ }
5376
+ // Compatibility fallback for direct applyChange() callers outside a session.
5377
+ const registry = getCustomElementRegistry();
5378
+ if (registry === null) {
4027
5379
  scheduledBindings.delete(binding);
5380
+ reportFailure(tagName, new Error("CustomElementRegistry is unavailable."));
5381
+ return;
5382
+ }
5383
+ getDefinitionCoordinator(registry).wait(tagName, () => {
4028
5384
  if (!binding.replaceNode.isConnected) {
4029
- return; // define を待つ間にノードが削除された
5385
+ scheduledBindings.delete(binding);
5386
+ return;
4030
5387
  }
4031
- applyChangeFromBindings([binding]);
4032
- }).catch((error) => {
4033
- console.error(`[@wcstack/state] deferred apply failed for <${tagName}>.`, error);
4034
- });
5388
+ applyLatest();
5389
+ }, reject);
4035
5390
  }
4036
5391
 
4037
5392
  const applyChangeByFirstSegment = {
@@ -4129,12 +5484,16 @@ function applyChange(binding, context) {
4129
5484
  ]));
4130
5485
  }
4131
5486
  }
5487
+ const bindingSession = getBindingSession(binding);
5488
+ if (bindingSession !== null && !bindingSession.shouldApplyState(binding)) {
5489
+ return;
5490
+ }
4132
5491
  if (binding.bindingType === "event") {
4133
5492
  return;
4134
5493
  }
4135
5494
  const customTag = getCustomElement(binding.replaceNode);
4136
5495
  if (customTag) {
4137
- if (customElements.get(customTag) === undefined) {
5496
+ if (getCustomElementRegistry()?.get(customTag) === undefined) {
4138
5497
  // 未 define のカスタム要素へは今は適用できない(accessor 未確立の要素に
4139
5498
  // 素の own property を書くと upgrade 後に class accessor を隠してしまう)。
4140
5499
  // whenDefined 後に最新 state 値で再適用する(two-way attach / deferred
@@ -4192,7 +5551,7 @@ function applyChange(binding, context) {
4192
5551
  * 最適化のため、以下のグループ化を行う:
4193
5552
  * 同じ stateNameとrootNode を持つバインディングをグループ化 → createState の呼び出しを削減
4194
5553
  */
4195
- function applyChangeFromBindings(bindings) {
5554
+ function applyChangeFromBindings(bindings, propagationContextByBinding) {
4196
5555
  let bindingIndex = 0;
4197
5556
  const appliedBindingSet = new Set();
4198
5557
  const newListValueByAbsAddress = new Map();
@@ -4234,6 +5593,7 @@ function applyChangeFromBindings(bindings) {
4234
5593
  // グループ内の binding は下の do/while が「解決済みルート === rootNode」を
4235
5594
  // 検証してから applyChange に渡す(applyChange 側の getRootNode 省略の根拠)
4236
5595
  sameRootVerified: true,
5596
+ propagationContextByBinding: propagationContextByBinding,
4237
5597
  };
4238
5598
  do {
4239
5599
  applyChange(binding, context);
@@ -4249,9 +5609,10 @@ function applyChangeFromBindings(bindings) {
4249
5609
  });
4250
5610
  }
4251
5611
  // Phase 2: 遅延されたselect.value/selectedIndex を適用
4252
- // applyChangeToProperty は context を参照しないため null を渡す
5612
+ // applyChangeToProperty は propagationContextByBinding 以外の context
5613
+ // 参照しないため、遅延分は最小 context を渡す
4253
5614
  for (const { binding, value } of deferredSelectBindings) {
4254
- applyChangeToProperty(binding, null, value);
5615
+ applyChangeToProperty(binding, { propagationContextByBinding }, value);
4255
5616
  }
4256
5617
  for (const [absAddress, newListValue] of newListValueByAbsAddress.entries()) {
4257
5618
  setLastListValueByAbsoluteStateAddress(absAddress, newListValue);
@@ -4263,217 +5624,41 @@ function applyChangeFromBindings(bindings) {
4263
5624
  }
4264
5625
  }
4265
5626
 
4266
- const handlerByHandlerKey$1 = new Map();
4267
- // binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
4268
- const bindingRegistry$1 = createHandlerBindingRegistry();
4269
- function getHandlerKey$1(binding, eventName) {
4270
- const filterKey = binding.inFilters.map(f => f.filterName + '(' + f.args.join(',') + ')').join('|');
4271
- return `${binding.stateName}::${binding.statePathName}::${eventName}::${filterKey}`;
4272
- }
4273
- function getEventName$1(binding) {
4274
- let eventName = 'input';
4275
- for (const modifier of binding.propModifiers) {
4276
- if (modifier.startsWith('on')) {
4277
- eventName = modifier.slice(2);
4278
- }
4279
- }
4280
- return eventName;
4281
- }
4282
- const radioEventHandlerFunction = (stateName, statePathName, inFilters) => (event) => {
4283
- const node = event.target;
4284
- if (node === null) {
4285
- console.warn(`[@wcstack/state] event.target is null.`);
4286
- return;
4287
- }
4288
- if (node.type !== 'radio') {
4289
- console.warn(`[@wcstack/state] event.target is not a radio input element.`);
4290
- return;
4291
- }
4292
- if (node.checked === false) {
4293
- return;
4294
- }
4295
- const newValue = node.value;
4296
- let filteredNewValue = newValue;
4297
- for (const filter of inFilters) {
4298
- filteredNewValue = filter.filterFn(filteredNewValue);
4299
- }
4300
- const rootNode = node.getRootNode();
4301
- const stateElement = getStateElementByName(rootNode, stateName);
4302
- if (stateElement === null) {
4303
- raiseError(`State element with name "${stateName}" not found for two-way binding.`);
4304
- }
4305
- const loopContext = getLoopContextByNode(node);
4306
- stateElement.createState("writable", (state) => {
4307
- state[setLoopContextSymbol](loopContext, () => {
4308
- state[statePathName] = filteredNewValue;
4309
- });
4310
- });
4311
- };
4312
- function attachRadioEventHandler(binding) {
4313
- if (binding.bindingType === "radio" && binding.propModifiers.indexOf('ro') === -1) {
4314
- const eventName = getEventName$1(binding);
4315
- const key = getHandlerKey$1(binding, eventName);
4316
- let radioEventHandler = handlerByHandlerKey$1.get(key);
4317
- if (typeof radioEventHandler === "undefined") {
4318
- radioEventHandler = radioEventHandlerFunction(binding.stateName, binding.statePathName, binding.inFilters);
4319
- handlerByHandlerKey$1.set(key, radioEventHandler);
4320
- }
4321
- binding.node.addEventListener(eventName, radioEventHandler);
4322
- bindingRegistry$1.add(key, binding);
4323
- return true;
4324
- }
4325
- return false;
4326
- }
4327
-
4328
- const handlerByHandlerKey = new Map();
4329
- // binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
4330
- const bindingRegistry = createHandlerBindingRegistry();
4331
- function getHandlerKey(binding, eventName) {
4332
- const filterKey = binding.inFilters.map(f => f.filterName + '(' + f.args.join(',') + ')').join('|');
4333
- return `${binding.stateName}::${binding.statePathName}::${eventName}::${filterKey}`;
4334
- }
4335
- function getEventName(binding) {
4336
- let eventName = 'input';
4337
- for (const modifier of binding.propModifiers) {
4338
- if (modifier.startsWith('on')) {
4339
- eventName = modifier.slice(2);
4340
- }
4341
- }
4342
- return eventName;
4343
- }
4344
- const checkboxEventHandlerFunction = (stateName, statePathName, inFilters) => (event) => {
4345
- const node = event.target;
4346
- if (node === null) {
4347
- console.warn(`[@wcstack/state] event.target is null.`);
4348
- return;
4349
- }
4350
- if (node.type !== 'checkbox') {
4351
- console.warn(`[@wcstack/state] event.target is not a checkbox input element.`);
4352
- return;
4353
- }
4354
- const checked = node.checked;
4355
- const newValue = node.value;
4356
- let filteredNewValue = newValue;
4357
- for (const filter of inFilters) {
4358
- filteredNewValue = filter.filterFn(filteredNewValue);
4359
- }
4360
- const rootNode = node.getRootNode();
4361
- const stateElement = getStateElementByName(rootNode, stateName);
4362
- if (stateElement === null) {
4363
- raiseError(`State element with name "${stateName}" not found for two-way binding.`);
4364
- }
4365
- const loopContext = getLoopContextByNode(node);
4366
- stateElement.createState("writable", (state) => {
4367
- state[setLoopContextSymbol](loopContext, () => {
4368
- let currentValue = state[statePathName];
4369
- if (Array.isArray(currentValue)) {
4370
- if (checked) {
4371
- if (currentValue.indexOf(filteredNewValue) === -1) {
4372
- state[statePathName] = currentValue.concat(filteredNewValue);
4373
- }
4374
- }
4375
- else {
4376
- const index = currentValue.indexOf(filteredNewValue);
4377
- if (index !== -1) {
4378
- state[statePathName] = currentValue.toSpliced(index, 1);
4379
- }
4380
- }
4381
- }
4382
- else {
4383
- if (checked) {
4384
- state[statePathName] = [filteredNewValue];
4385
- }
4386
- else {
4387
- state[statePathName] = [];
4388
- }
4389
- }
4390
- });
4391
- });
4392
- };
4393
- function attachCheckboxEventHandler(binding) {
4394
- if (binding.bindingType === "checkbox" && binding.propModifiers.indexOf('ro') === -1) {
4395
- const eventName = getEventName(binding);
4396
- const key = getHandlerKey(binding, eventName);
4397
- let checkboxEventHandler = handlerByHandlerKey.get(key);
4398
- if (typeof checkboxEventHandler === "undefined") {
4399
- checkboxEventHandler = checkboxEventHandlerFunction(binding.stateName, binding.statePathName, binding.inFilters);
4400
- handlerByHandlerKey.set(key, checkboxEventHandler);
4401
- }
4402
- binding.node.addEventListener(eventName, checkboxEventHandler);
4403
- bindingRegistry.add(key, binding);
4404
- return true;
4405
- }
4406
- return false;
4407
- }
4408
-
4409
- function _initializeBindings(allBindings) {
4410
- for (const binding of allBindings) {
4411
- // replace node
4412
- replaceToReplaceNode(binding);
4413
- // event
4414
- if (attachEventHandler(binding)) {
4415
- continue;
4416
- }
4417
- // event token (element → state)
4418
- if (attachEventTokenHandler(binding)) {
4419
- continue;
4420
- }
4421
- // two-way binding
4422
- attachTwowayEventHandler(binding);
4423
- // radio binding
4424
- attachRadioEventHandler(binding);
4425
- // checkbox binding
4426
- attachCheckboxEventHandler(binding);
4427
- }
4428
- }
4429
- function _registerAbsoluteAddresses(allBindings) {
4430
- for (const binding of allBindings) {
4431
- const absoluteStateAddress = getAbsoluteStateAddressByBinding(binding);
4432
- addBindingByAbsoluteStateAddress(absoluteStateAddress, binding);
4433
- const rootNode = binding.replaceNode.getRootNode();
4434
- const stateElement = getStateElementByName(rootNode, binding.stateName);
4435
- if (stateElement === null) {
4436
- raiseError(`State element with name "${binding.stateName}" not found for binding.`);
4437
- }
4438
- if (binding.bindingType !== 'event') {
4439
- stateElement.setPathInfo(binding.statePathName, binding.bindingType);
4440
- }
4441
- }
4442
- }
4443
- function _scheduleDeferredSpreads(deferredSpreads, parentLoopContext) {
5627
+ function scheduleDeferredSpreads(deferredSpreads, parentLoopContext, session) {
4444
5628
  for (const entry of deferredSpreads) {
4445
- customElements.whenDefined(entry.tagName).then(() => {
4446
- if (!entry.node.isConnected)
4447
- return; // node was removed before class became ready
5629
+ session.deferUntilDefined(entry.node, entry.tagName, () => {
4448
5630
  const bindings = processDeferredNode(entry);
4449
5631
  if (bindings.length === 0)
4450
5632
  return;
4451
5633
  setLoopContextByNode(entry.node, parentLoopContext);
4452
- _initializeBindings(bindings);
4453
- _registerAbsoluteAddresses(bindings);
4454
- applyChangeFromBindings(bindings);
4455
- }).catch((error) => {
5634
+ const initialized = session.initialize(bindings);
5635
+ applyChangeFromBindings(initialized);
5636
+ }, (error) => {
4456
5637
  console.error(`[@wcstack/state] deferred spread failed for <${entry.tagName}>.`, error);
4457
5638
  });
4458
5639
  }
4459
5640
  }
4460
5641
  function initializeBindings(root, parentLoopContext) {
4461
5642
  const [subscriberNodes, allBindings, deferredSpreads] = collectNodesAndBindingInfos(root);
5643
+ const session = getOrCreateBindingSession(root);
4462
5644
  for (const node of subscriberNodes) {
4463
5645
  setLoopContextByNode(node, parentLoopContext);
4464
5646
  }
4465
- _initializeBindings(allBindings);
4466
- _registerAbsoluteAddresses(allBindings);
4467
- // apply all at once
4468
- applyChangeFromBindings(allBindings);
4469
- _scheduleDeferredSpreads(deferredSpreads, parentLoopContext);
5647
+ const initialized = session.initialize(allBindings);
5648
+ applyChangeFromBindings(initialized);
5649
+ scheduleDeferredSpreads(deferredSpreads, parentLoopContext, session);
4470
5650
  }
4471
5651
  function initializeBindingsByFragment(root, nodeInfos) {
4472
5652
  const [subscriberNodes, allBindings] = collectNodesAndBindingInfosByFragment(root, nodeInfos);
4473
- _initializeBindings(allBindings);
5653
+ const session = new BindingSession();
5654
+ const initialized = session.initialize(allBindings, {
5655
+ registerAddress: false,
5656
+ applyOnReconnect: false,
5657
+ });
4474
5658
  return {
4475
5659
  nodes: subscriberNodes,
4476
- bindingInfos: allBindings,
5660
+ bindingInfos: initialized,
5661
+ bindingSession: session,
4477
5662
  };
4478
5663
  }
4479
5664
 
@@ -4873,12 +6058,20 @@ async function buildBindings(root) {
4873
6058
  }
4874
6059
  }
4875
6060
 
4876
- var version = "1.20.0";
6061
+ var version = "1.21.0";
4877
6062
  var pkg = {
4878
6063
  version: version};
4879
6064
 
4880
6065
  const VERSION = pkg.version;
4881
6066
 
6067
+ /**
6068
+ * Browser builds use the native HTMLElement. Headless runtimes receive an
6069
+ * inert base so the public module can be imported without installing DOM
6070
+ * globals; constructing components remains a browser-only operation.
6071
+ */
6072
+ const HTMLElementBase = (typeof HTMLElement === "undefined" ? class {
6073
+ } : HTMLElement);
6074
+
4882
6075
  // SSR コメントパターン
4883
6076
  const SSR_PLACEHOLDER_COMMENT = /^@@wcs-(?:for|if|elseif|else):[^-]/;
4884
6077
  const SSR_BLOCK_START = /^@@wcs-(for|if|elseif|else)-start:(.+)$/;
@@ -4899,7 +6092,7 @@ function escapeJsonForScript(json) {
4899
6092
  .replace(/\u2028/g, '\\u2028')
4900
6093
  .replace(/\u2029/g, '\\u2029');
4901
6094
  }
4902
- class Ssr extends HTMLElement {
6095
+ class Ssr extends HTMLElementBase {
4903
6096
  _stateData = null;
4904
6097
  _templates = null;
4905
6098
  _hydrateProps = null;
@@ -5276,8 +6469,10 @@ function collectSsrBlocks(root) {
5276
6469
  * 元の位置に戻す。
5277
6470
  */
5278
6471
  function collectBindingsFromLiveNodes(nodes) {
5279
- if (nodes.length === 0)
5280
- return { bindingInfos: [], subscriberNodes: [] };
6472
+ const bindingSession = new BindingSession();
6473
+ if (nodes.length === 0) {
6474
+ return { bindingInfos: [], subscriberNodes: [], bindingSession };
6475
+ }
5281
6476
  // ノードの元の位置を記録
5282
6477
  const parent = nodes[0].parentNode;
5283
6478
  const nextSibling = nodes[nodes.length - 1].nextSibling;
@@ -5288,17 +6483,10 @@ function collectBindingsFromLiveNodes(nodes) {
5288
6483
  }
5289
6484
  // バインディング収集
5290
6485
  const [subscriberNodes, allBindings] = collectNodesAndBindingInfos(wrapper);
5291
- // _initializeBindings 相当の処理
5292
- for (const binding of allBindings) {
5293
- replaceToReplaceNode(binding);
5294
- if (attachEventHandler(binding))
5295
- continue;
5296
- if (attachEventTokenHandler(binding))
5297
- continue;
5298
- attachTwowayEventHandler(binding);
5299
- attachRadioEventHandler(binding);
5300
- attachCheckboxEventHandler(binding);
5301
- }
6486
+ const bindingInfos = bindingSession.initialize(allBindings, {
6487
+ registerAddress: false,
6488
+ applyOnReconnect: false,
6489
+ });
5302
6490
  // 元の位置に戻す
5303
6491
  if (parent) {
5304
6492
  while (wrapper.firstChild) {
@@ -5306,8 +6494,9 @@ function collectBindingsFromLiveNodes(nodes) {
5306
6494
  }
5307
6495
  }
5308
6496
  return {
5309
- bindingInfos: allBindings,
6497
+ bindingInfos,
5310
6498
  subscriberNodes,
6499
+ bindingSession,
5311
6500
  };
5312
6501
  }
5313
6502
  /**
@@ -5321,7 +6510,8 @@ function hydrateBlocks(root, blocks) {
5321
6510
  continue;
5322
6511
  const content = createContentFromNodes(block.nodes);
5323
6512
  // Content のバインディングを収集
5324
- const { bindingInfos, subscriberNodes } = collectBindingsFromLiveNodes(block.nodes);
6513
+ const { bindingInfos, subscriberNodes, bindingSession } = collectBindingsFromLiveNodes(block.nodes);
6514
+ setBindingSessionByContent(content, bindingSession);
5325
6515
  // Content 内のノードに data-wcs-completed を付与
5326
6516
  // (メインの collectNodesAndBindingInfos で重複登録されないようにする)
5327
6517
  for (const node of subscriberNodes) {
@@ -5351,10 +6541,11 @@ function hydrateBlocks(root, blocks) {
5351
6541
  const stateAddress = createStateAddress(pathInfo, listIndex);
5352
6542
  // ILoopContext は IStateAddress + listIndex なので、stateAddress をそのまま使う
5353
6543
  bindLoopContextToContent(content, stateAddress);
5354
- for (const binding of bindingInfos) {
5355
- const absAddr = getAbsoluteStateAddressByBinding(binding);
5356
- addBindingByAbsoluteStateAddress(absAddr, binding);
5357
- }
6544
+ bindingSession.initialize(bindingInfos, {
6545
+ registerAddress: true,
6546
+ registerPathInfo: false,
6547
+ applyOnReconnect: false,
6548
+ });
5358
6549
  // listIndex を UUID ごとに収集(後で setListIndexesByList に渡す)
5359
6550
  let indexes = listIndexesByUuid.get(block.uuid);
5360
6551
  if (!indexes) {
@@ -5368,11 +6559,11 @@ function hydrateBlocks(root, blocks) {
5368
6559
  const placeholderComment = findPlaceholderComment(root, block.type, block.uuid);
5369
6560
  if (placeholderComment) {
5370
6561
  setContentByNode(placeholderComment, content);
5371
- // バインディングをアドレスに登録
5372
- for (const binding of bindingInfos) {
5373
- const absAddr = getAbsoluteStateAddressByBinding(binding);
5374
- addBindingByAbsoluteStateAddress(absAddr, binding);
5375
- }
6562
+ bindingSession.initialize(bindingInfos, {
6563
+ registerAddress: true,
6564
+ registerPathInfo: false,
6565
+ applyOnReconnect: false,
6566
+ });
5376
6567
  }
5377
6568
  }
5378
6569
  }
@@ -5529,17 +6720,11 @@ async function hydrateBindings(root) {
5529
6720
  // バインディングを構造系とそれ以外に分離
5530
6721
  const normalBindings = [];
5531
6722
  const structuralBindings = [];
5532
- for (const binding of allBindings) {
5533
- replaceToReplaceNode(binding);
5534
- if (attachEventHandler(binding)) {
5535
- continue;
5536
- }
5537
- if (attachEventTokenHandler(binding)) {
6723
+ const bindingSession = getOrCreateBindingSession(document.body);
6724
+ const initializedBindings = bindingSession.initialize(allBindings);
6725
+ for (const binding of initializedBindings) {
6726
+ if (binding.bindingType === "event")
5538
6727
  continue;
5539
- }
5540
- attachTwowayEventHandler(binding);
5541
- attachRadioEventHandler(binding);
5542
- attachCheckboxEventHandler(binding);
5543
6728
  if (STRUCTURAL_TYPES.has(binding.bindingType)) {
5544
6729
  structuralBindings.push(binding);
5545
6730
  }
@@ -5551,19 +6736,6 @@ async function hydrateBindings(root) {
5551
6736
  normalBindings.push(binding);
5552
6737
  }
5553
6738
  }
5554
- // 全バインディング(通常 + 構造)をアドレスに登録
5555
- for (const binding of [...normalBindings, ...structuralBindings]) {
5556
- const absoluteStateAddress = getAbsoluteStateAddressByBinding(binding);
5557
- addBindingByAbsoluteStateAddress(absoluteStateAddress, binding);
5558
- const rootNode = binding.replaceNode.getRootNode();
5559
- const stateElement = getStateElementByName(rootNode, binding.stateName);
5560
- if (stateElement === null) {
5561
- raiseError(`State element with name "${binding.stateName}" not found for binding.`);
5562
- }
5563
- if (binding.bindingType !== 'event') {
5564
- stateElement.setPathInfo(binding.statePathName, binding.bindingType);
5565
- }
5566
- }
5567
6739
  // for バインディングの lastListValue を初期値として設定
5568
6740
  // (次回の状態変化時に差分計算の基準になる)
5569
6741
  for (const binding of structuralBindings) {
@@ -5726,31 +6898,73 @@ function notifyUpdateBatchListeners(batch) {
5726
6898
  }
5727
6899
  }
5728
6900
  class Updater {
5729
- _queueAbsoluteAddresses = [];
6901
+ _queueUpdateRecords = [];
5730
6902
  constructor() {
5731
6903
  }
5732
- enqueueAbsoluteAddress(absoluteAddress) {
5733
- const requireStartProcess = this._queueAbsoluteAddresses.length === 0;
5734
- this._queueAbsoluteAddresses.push(absoluteAddress);
6904
+ enqueueAbsoluteAddress(absoluteAddress, context = null) {
6905
+ const requireStartProcess = this._queueUpdateRecords.length === 0;
6906
+ this._queueUpdateRecords.push({ absoluteAddress, context });
5735
6907
  if (requireStartProcess) {
5736
6908
  queueMicrotask(() => {
5737
- const absoluteAddresses = this._queueAbsoluteAddresses;
5738
- this._queueAbsoluteAddresses = [];
5739
- this._applyChange(absoluteAddresses);
6909
+ const updateRecords = this._queueUpdateRecords;
6910
+ this._queueUpdateRecords = [];
6911
+ this._applyChange(updateRecords);
5740
6912
  });
5741
6913
  }
5742
6914
  }
5743
6915
  // テスト用に公開
5744
- testApplyChange(absoluteAddresses) {
5745
- this._applyChange(absoluteAddresses);
6916
+ testApplyChange(absoluteAddresses, contexts) {
6917
+ this._applyChange(absoluteAddresses.map((absoluteAddress, index) => ({
6918
+ absoluteAddress,
6919
+ context: contexts?.[index] ?? null,
6920
+ })));
5746
6921
  }
5747
- _applyChange(absoluteAddresses) {
6922
+ _applyChange(updateRecords) {
5748
6923
  // Note: AbsoluteStateAddress はキャッシュされているため、
5749
6924
  // 同一の (stateName, address) は同じインスタンスとなり、
5750
- // Set による重複排除が正しく機能する
5751
- const absoluteAddressSet = new Set(absoluteAddresses);
6925
+ // Map / Set による重複排除が正しく機能する。
6926
+ // coalescing last-write-wins: 同じ address は最後の update の
6927
+ // (値は state 側が既に保持) context をそのまま採用する(設計書 §4.1)。
6928
+ // visitedEdges の合成や synthetic transaction への置換は行わない。
6929
+ const contextByAbsoluteAddress = new Map();
6930
+ for (const record of updateRecords) {
6931
+ const previous = contextByAbsoluteAddress.get(record.absoluteAddress);
6932
+ if (devtoolsSink !== null
6933
+ && typeof previous !== "undefined" && previous !== null
6934
+ && record.context !== null
6935
+ && previous.transactionId !== record.context.transactionId) {
6936
+ devtoolsSink({
6937
+ type: "propagation:coalesced",
6938
+ absoluteAddress: record.absoluteAddress,
6939
+ droppedTransactionId: previous.transactionId,
6940
+ winnerTransactionId: record.context.transactionId,
6941
+ });
6942
+ }
6943
+ contextByAbsoluteAddress.set(record.absoluteAddress, record.context);
6944
+ }
5752
6945
  const processBindings = [];
5753
- for (const absoluteAddress of absoluteAddressSet) {
6946
+ const propagationContextByBinding = new Map();
6947
+ for (const [absoluteAddress, context] of contextByAbsoluteAddress) {
6948
+ if (context !== null && context.hop >= MAX_PROPAGATION_HOPS) {
6949
+ // hop 上限超過: この transaction の未処理 record だけを quarantine する。
6950
+ // 既に適用した値は戻さず、updater から例外は投げない(設計書 §4 規則 6)。
6951
+ console.error(`[@wcstack/state] propagation hop limit exceeded; update record quarantined.`, {
6952
+ path: absoluteAddress.absolutePathInfo.pathInfo.path,
6953
+ stateName: absoluteAddress.absolutePathInfo.stateName,
6954
+ transactionId: context.transactionId,
6955
+ hop: context.hop,
6956
+ maxHops: MAX_PROPAGATION_HOPS,
6957
+ });
6958
+ if (devtoolsSink !== null) {
6959
+ devtoolsSink({
6960
+ type: "propagation:hop-limit",
6961
+ absoluteAddress,
6962
+ transactionId: context.transactionId,
6963
+ hop: context.hop,
6964
+ });
6965
+ }
6966
+ continue;
6967
+ }
5754
6968
  // peek: バインディングの無いアドレス(リスト置換で enqueue される中間
5755
6969
  // アドレス等)に空 Set を生成・蓄積しない
5756
6970
  const bindings = peekBindingSetByAbsoluteStateAddress(absoluteAddress);
@@ -5763,12 +6977,22 @@ class Updater {
5763
6977
  continue;
5764
6978
  }
5765
6979
  processBindings.push(binding);
6980
+ if (context !== null) {
6981
+ propagationContextByBinding.set(binding, context);
6982
+ }
5766
6983
  }
5767
6984
  }
5768
- applyChangeFromBindings(processBindings);
6985
+ // context が無い場合は従来どおり 1 引数で呼ぶ(呼び出し契約の互換維持)
6986
+ if (propagationContextByBinding.size > 0) {
6987
+ applyChangeFromBindings(processBindings, propagationContextByBinding);
6988
+ }
6989
+ else {
6990
+ applyChangeFromBindings(processBindings);
6991
+ }
5769
6992
  // drain 終了フック: binding 適用後に dedup 済みバッチを通知する(設計書 §3-2)。
5770
6993
  // testApplyChange も同じ _applyChange を通るため、テストから同期に駆動できる。
5771
- notifyUpdateBatchListeners(absoluteAddressSet);
6994
+ // quarantine された address も state 値は適用済みのため通知対象に含める。
6995
+ notifyUpdateBatchListeners(new Set(contextByAbsoluteAddress.keys()));
5772
6996
  }
5773
6997
  }
5774
6998
  const updater = new Updater();
@@ -7643,6 +8867,29 @@ function getContextListIndex(handler, structuredPath) {
7643
8867
  return address.listIndex?.at(index) ?? null;
7644
8868
  }
7645
8869
 
8870
+ /**
8871
+ * Reports whether an address has been initialized, independently of its value.
8872
+ * In particular, an own slot containing `undefined` is initialized while a
8873
+ * missing slot is not.
8874
+ */
8875
+ function hasByAddress(target, address, receiver, handler) {
8876
+ if (address.pathInfo.path in target)
8877
+ return true;
8878
+ const parentAddress = address.parentAddress;
8879
+ if (parentAddress === null)
8880
+ return false;
8881
+ const parentValue = getByAddress(target, parentAddress, receiver, handler);
8882
+ if (parentValue === null || (typeof parentValue !== "object" && typeof parentValue !== "function")) {
8883
+ return false;
8884
+ }
8885
+ const lastSegment = address.pathInfo.lastSegment;
8886
+ if (lastSegment === WILDCARD) {
8887
+ const index = address.listIndex?.index;
8888
+ return typeof index === "number" && index in parentValue;
8889
+ }
8890
+ return lastSegment in parentValue;
8891
+ }
8892
+
7646
8893
  const swapInfoByStateAddress = new WeakMap();
7647
8894
  function getSwapInfoByAddress(address) {
7648
8895
  return swapInfoByStateAddress.get(address) ?? null;
@@ -7903,7 +9150,10 @@ function _setByAddress(target, address, absAddress, value, receiver, handler) {
7903
9150
  }
7904
9151
  }
7905
9152
  else {
7906
- const parentAddress = address.parentAddress ?? raiseError(`address.parentAddress is undefined path: ${address.pathInfo.path}`);
9153
+ const parentAddress = address.parentAddress;
9154
+ if (parentAddress === null) {
9155
+ return Reflect.set(target, address.pathInfo.path, value);
9156
+ }
7907
9157
  const parentValue = getByAddress(target, parentAddress, receiver, handler);
7908
9158
  const lastSegment = address.pathInfo.segments[address.pathInfo.segments.length - 1];
7909
9159
  if (lastSegment === WILDCARD) {
@@ -7916,8 +9166,15 @@ function _setByAddress(target, address, absAddress, value, receiver, handler) {
7916
9166
  }
7917
9167
  }
7918
9168
  finally {
9169
+ // Phase 3: 書き込み時点の因果 context を update record に付与する。
9170
+ // binding 経由の書き込みは呼び出し元の dynamic scope から context を引き継ぎ、
9171
+ // binding 外からの API update は新しい transaction を開始する(設計書 §4 規則 1)。
9172
+ // 依存 walk で enqueue される派生アドレスも同じ書き込みの因果に属する。
9173
+ const propagationContext = config.enablePropagationContext
9174
+ ? (getCurrentPropagationContext() ?? beginPropagationTransaction(-1))
9175
+ : null;
7919
9176
  const updater = getUpdater();
7920
- updater.enqueueAbsoluteAddress(absAddress);
9177
+ updater.enqueueAbsoluteAddress(absAddress, propagationContext);
7921
9178
  // 依存関係のあるキャッシュを無効化(ダーティ)、更新対象として登録
7922
9179
  walkDependency(handler.stateName, handler.stateElement, address, handler.stateElement.staticDependency, handler.stateElement.dynamicDependency, handler.stateElement.listPaths, receiver, "new", (depAddress) => {
7923
9180
  // キャッシュを無効化(ダーティ)
@@ -7927,7 +9184,7 @@ function _setByAddress(target, address, absAddress, value, receiver, handler) {
7927
9184
  const absDepAddress = createAbsoluteStateAddress(absDepPathInfo, depAddress.listIndex);
7928
9185
  dirtyCacheEntryByAbsoluteStateAddress(absDepAddress);
7929
9186
  // 更新対象として登録
7930
- updater.enqueueAbsoluteAddress(absDepAddress);
9187
+ updater.enqueueAbsoluteAddress(absDepAddress, propagationContext);
7931
9188
  },
7932
9189
  // リスト置換時は追加行・位置変更行のみ展開する(未変更行の再訪を省く。
7933
9190
  // $postUpdate の手動リフレッシュは従来通り全行展開のまま)
@@ -7982,7 +9239,7 @@ function setByAddress(target, address, value, receiver, handler) {
7982
9239
  let devHasOldValue = false;
7983
9240
  if (config.sameValueGuard && (value === null || typeof value !== "object")) {
7984
9241
  const oldValue = getByAddress(target, address, receiver, handler);
7985
- if (Object.is(oldValue, value)) {
9242
+ if (hasByAddress(target, address, receiver, handler) && Object.is(oldValue, value)) {
7986
9243
  return true;
7987
9244
  }
7988
9245
  devOldValue = oldValue;
@@ -8470,6 +9727,11 @@ function get(target, prop, receiver, handler) {
8470
9727
  return getByAddress(target, address, receiver, handler);
8471
9728
  };
8472
9729
  }
9730
+ case hasByAddressSymbol: {
9731
+ return (address) => {
9732
+ return hasByAddress(target, address, receiver, handler);
9733
+ };
9734
+ }
8473
9735
  case setByAddressSymbol: {
8474
9736
  return (address, value) => {
8475
9737
  return setByAddress(target, address, value, receiver, handler);
@@ -9020,7 +10282,7 @@ function getStateInfo(state) {
9020
10282
  getterPaths, setterPaths
9021
10283
  };
9022
10284
  }
9023
- class State extends HTMLElement {
10285
+ class State extends HTMLElementBase {
9024
10286
  static hasConnectedCallbackPromise = true;
9025
10287
  static getBindingsReady(rootNode) {
9026
10288
  return getBindingsReady(rootNode);
@@ -9767,5 +11029,133 @@ function getWcsManifest() {
9767
11029
  };
9768
11030
  }
9769
11031
 
9770
- export { Ssr, VERSION, WCS_MANIFEST_VERSION, bootstrapState, buildBindings, builtinFilterMeta, defineState, getBindingsReady, getConfig, getWcsManifest };
11032
+ /**
11033
+ * contract/contractAnalyzer.ts
11034
+ *
11035
+ * Phase 5b(09-remediation-design.md §5b / §7.1 dev runtime / §6 contract trace)の
11036
+ * opt-in dev-time analyzer。実際に登録済みの custom element の `static wcBindable`
11037
+ * 宣言(= 実行時の正本)を、利用者が渡した sidecar manifest と突き合わせ、drift を
11038
+ * DevTools trace(`contract:*`)へ流す。
11039
+ *
11040
+ * 完了条件「無効時の runtime 挙動・cost が不変」: `analyzeContract` は
11041
+ * `config.enableContractAnalyzer` が false のとき即 return し、manifest を一切走査
11042
+ * しない(hot path には一切フックしない — 純粋な on-demand API)。
11043
+ *
11044
+ * pure な core(`analyzeManifestContract`)は宣言解決と emit を注入で受けるためテスト可能。
11045
+ */
11046
+ /** runtime analyzer が解釈する manifest namespace。これ以外は unsupported-extension。 */
11047
+ const KNOWN_NAMESPACES = new Set([
11048
+ "wcstack.types",
11049
+ "wcstack.async",
11050
+ "wcstack.platformCapabilities",
11051
+ "wcstack.application",
11052
+ ]);
11053
+ const EMPTY = Object.freeze([]);
11054
+ /**
11055
+ * opt-in dev-time contract analysis。無効時はゼロコスト(即 return・manifest 非走査)。
11056
+ * 有効時は live 宣言と manifest を突き合わせ、`contract:*` trace を返しつつ、DevTools
11057
+ * sink が接続されていれば同時に流す。
11058
+ */
11059
+ function analyzeContract(manifest) {
11060
+ if (!config.enableContractAnalyzer)
11061
+ return EMPTY;
11062
+ const events = [];
11063
+ const emit = (event) => {
11064
+ events.push(event);
11065
+ if (devtoolsSink !== null)
11066
+ devtoolsSink(event);
11067
+ };
11068
+ analyzeManifestContract(manifest, resolveLiveDeclaration, emit);
11069
+ return events;
11070
+ }
11071
+ /**
11072
+ * pure core。`resolveDeclaration(tag)` は該当タグの live 宣言(未登録なら null)を返す。
11073
+ * emit は生成した trace を受ける。config フラグは見ない(呼び出し側が guard 済み)。
11074
+ */
11075
+ function analyzeManifestContract(manifest, resolveDeclaration, emit) {
11076
+ const extensions = manifest.manifestExtensions;
11077
+ if (extensions === null || typeof extensions !== "object")
11078
+ return;
11079
+ // 未知 namespace は runtime が解釈しない → unsupported-extension。
11080
+ for (const namespace of Object.keys(extensions)) {
11081
+ if (!KNOWN_NAMESPACES.has(namespace)) {
11082
+ emit({ type: "contract:unsupported-extension", namespace });
11083
+ }
11084
+ }
11085
+ const components = extensions["wcstack.types"]?.components;
11086
+ if (components === undefined || components === null)
11087
+ return;
11088
+ for (const [tag, component] of Object.entries(components)) {
11089
+ const live = resolveDeclaration(tag);
11090
+ emit({ type: "contract:manifest-read", tag, loaded: live !== null });
11091
+ if (live === null) {
11092
+ // manifest が宣言するタグが実行時に登録されていない = component-not-loaded drift。
11093
+ emit({ type: "contract:drift", reason: "component-not-loaded", tag });
11094
+ continue;
11095
+ }
11096
+ checkComponentDrift(tag, component, live, emit);
11097
+ }
11098
+ }
11099
+ function checkComponentDrift(tag, rawComponent, live, emit) {
11100
+ // 壊れた manifest(component が null / primitive)でも analyzer 全体を落とさない。
11101
+ const component = rawComponent !== null && typeof rawComponent === "object" ? rawComponent : {};
11102
+ for (const [member, observable] of Object.entries(component.observables ?? {})) {
11103
+ if (!live.propertyEvents.has(member)) {
11104
+ emit({ type: "contract:drift", reason: "missing-member", tag, member });
11105
+ continue;
11106
+ }
11107
+ const liveEvent = live.propertyEvents.get(member);
11108
+ const sidecarEvent = observable?.event;
11109
+ if (typeof sidecarEvent === "string" && sidecarEvent !== liveEvent) {
11110
+ emit({ type: "contract:drift", reason: "event-mismatch", tag, member, sidecarEvent, liveEvent });
11111
+ }
11112
+ }
11113
+ for (const member of Object.keys(component.inputs ?? {})) {
11114
+ if (!live.inputs.has(member)) {
11115
+ emit({ type: "contract:drift", reason: "missing-member", tag, member });
11116
+ }
11117
+ }
11118
+ for (const member of Object.keys(component.commands ?? {})) {
11119
+ if (!live.commands.has(member)) {
11120
+ emit({ type: "contract:drift", reason: "missing-member", tag, member });
11121
+ }
11122
+ }
11123
+ }
11124
+ /**
11125
+ * 登録済み custom element の `static wcBindable` を drift 照合用に索引化する。
11126
+ * 未登録・非 wc-bindable は null(= component-not-loaded)。
11127
+ */
11128
+ function resolveLiveDeclaration(tag) {
11129
+ const registry = getCustomElementRegistry();
11130
+ const ctor = registry?.get(tag);
11131
+ if (ctor === undefined)
11132
+ return null;
11133
+ const declaration = ctor.wcBindable;
11134
+ if (declaration === null
11135
+ || typeof declaration !== "object"
11136
+ || declaration.protocol !== "wc-bindable") {
11137
+ return null;
11138
+ }
11139
+ const decl = declaration;
11140
+ // 各配列は非配列(object 等)でも落ちないよう Array.isArray で container を守る。
11141
+ const propertyEvents = new Map();
11142
+ for (const property of Array.isArray(decl.properties) ? decl.properties : []) {
11143
+ if (typeof property?.name === "string" && typeof property.event === "string") {
11144
+ propertyEvents.set(property.name, property.event);
11145
+ }
11146
+ }
11147
+ const inputs = new Set();
11148
+ for (const input of Array.isArray(decl.inputs) ? decl.inputs : []) {
11149
+ if (typeof input?.name === "string")
11150
+ inputs.add(input.name);
11151
+ }
11152
+ const commands = new Set();
11153
+ for (const command of Array.isArray(decl.commands) ? decl.commands : []) {
11154
+ if (typeof command?.name === "string")
11155
+ commands.add(command.name);
11156
+ }
11157
+ return { propertyEvents, inputs, commands };
11158
+ }
11159
+
11160
+ export { Ssr, VERSION, WCS_MANIFEST_VERSION, analyzeContract, bootstrapState, buildBindings, builtinFilterMeta, defineState, getBindingsReady, getConfig, getWcsManifest };
9771
11161
  //# sourceMappingURL=index.esm.js.map