@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/README.ja.md +17 -2
- package/README.md +18 -2
- package/dist/index.d.ts +333 -3
- package/dist/index.esm.js +2204 -814
- package/dist/index.esm.js.map +1 -1
- package/dist/index.esm.min.js +1 -1
- package/dist/index.esm.min.js.map +1 -1
- package/package.json +2 -1
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
|
-
|
|
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
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
386
|
-
if (seen.has(
|
|
503
|
+
for (const name of bindable.knownProperties.keys()) {
|
|
504
|
+
if (seen.has(name))
|
|
387
505
|
continue;
|
|
388
|
-
seen.add(
|
|
389
|
-
const entry = makeExpandedEntry(
|
|
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
|
|
396
|
-
if (seen.has(
|
|
513
|
+
for (const name of bindable.declaredInputs.keys()) {
|
|
514
|
+
if (seen.has(name))
|
|
397
515
|
continue;
|
|
398
|
-
seen.add(
|
|
399
|
-
const entry = makeExpandedEntry(
|
|
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
|
-
|
|
1750
|
-
|
|
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
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
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
|
|
1759
|
-
if (typeof this.
|
|
1760
|
-
return this.
|
|
1946
|
+
get parentAbsoluteAddress() {
|
|
1947
|
+
if (typeof this._parentAbsoluteAddress !== 'undefined') {
|
|
1948
|
+
return this._parentAbsoluteAddress;
|
|
1761
1949
|
}
|
|
1762
|
-
const
|
|
1763
|
-
if (
|
|
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.
|
|
1962
|
+
return this._parentAbsoluteAddress = createAbsoluteStateAddress(parentAbsolutePathInfo, parentListIndex);
|
|
1775
1963
|
}
|
|
1776
1964
|
}
|
|
1777
|
-
function
|
|
1965
|
+
function createAbsoluteStateAddress(absolutePathInfo, listIndex) {
|
|
1778
1966
|
if (listIndex === null) {
|
|
1779
|
-
let cached = _cacheNullListIndex$1.get(
|
|
1967
|
+
let cached = _cacheNullListIndex$1.get(absolutePathInfo);
|
|
1780
1968
|
if (typeof cached !== "undefined") {
|
|
1781
1969
|
return cached;
|
|
1782
1970
|
}
|
|
1783
|
-
cached = new
|
|
1784
|
-
_cacheNullListIndex$1.set(
|
|
1971
|
+
cached = new AbsoluteStateAddress(absolutePathInfo, null);
|
|
1972
|
+
_cacheNullListIndex$1.set(absolutePathInfo, cached);
|
|
1785
1973
|
return cached;
|
|
1786
1974
|
}
|
|
1787
1975
|
else {
|
|
1788
|
-
let
|
|
1789
|
-
if (typeof
|
|
1790
|
-
|
|
1791
|
-
_cache$
|
|
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 =
|
|
1981
|
+
let cached = cacheByAbsolutePathInfo.get(absolutePathInfo);
|
|
1794
1982
|
if (typeof cached !== "undefined") {
|
|
1795
1983
|
return cached;
|
|
1796
1984
|
}
|
|
1797
|
-
cached = new
|
|
1798
|
-
|
|
1985
|
+
cached = new AbsoluteStateAddress(absolutePathInfo, listIndex);
|
|
1986
|
+
cacheByAbsolutePathInfo.set(absolutePathInfo, cached);
|
|
1799
1987
|
return cached;
|
|
1800
1988
|
}
|
|
1801
1989
|
}
|
|
1802
1990
|
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
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
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
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
|
-
|
|
1837
|
-
|
|
2011
|
+
if (pathInfo.wildcardCount === 1
|
|
2012
|
+
&& targetPathInfo.wildcardCount > 0
|
|
2013
|
+
&& targetPathInfo.wildcardPathSet.has(pathInfo.path)) {
|
|
2014
|
+
return 1;
|
|
1838
2015
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
this._subscribers.delete(fn);
|
|
1843
|
-
};
|
|
2016
|
+
if (pathInfo.id < targetPathInfo.id) {
|
|
2017
|
+
path1 = pathInfo;
|
|
2018
|
+
path2 = targetPathInfo;
|
|
1844
2019
|
}
|
|
1845
|
-
|
|
1846
|
-
|
|
2020
|
+
else {
|
|
2021
|
+
path1 = targetPathInfo;
|
|
2022
|
+
path2 = pathInfo;
|
|
1847
2023
|
}
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
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
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
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
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1886
|
-
|
|
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
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
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
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
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
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
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
|
|
1975
|
-
return `${binding.stateName}::${binding.statePathName}::${
|
|
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
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
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
|
|
2316
|
+
raiseError(`State element with name "${stateName}" not found for two-way binding.`);
|
|
1987
2317
|
}
|
|
1988
2318
|
const loopContext = getLoopContextByNode(node);
|
|
1989
|
-
|
|
1990
|
-
stateElement.createStateAsync("writable", async (state) => {
|
|
2319
|
+
stateElement.createState("writable", (state) => {
|
|
1991
2320
|
state[setLoopContextSymbol](loopContext, () => {
|
|
1992
|
-
|
|
1993
|
-
if (
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
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
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
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
|
|
2010
|
-
if (
|
|
2011
|
-
|
|
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
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
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.
|
|
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 =
|
|
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 =
|
|
2202
|
-
if (bindable?.
|
|
2203
|
-
|
|
2204
|
-
|
|
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
|
|
2915
|
+
return null;
|
|
2209
2916
|
}
|
|
2210
2917
|
|
|
2211
|
-
const handlerByHandlerKey
|
|
2918
|
+
const handlerByHandlerKey = new Map();
|
|
2212
2919
|
// binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
|
|
2213
|
-
const bindingRegistry
|
|
2920
|
+
const bindingRegistry = createHandlerBindingRegistry();
|
|
2921
|
+
const producerValueObserversByNode = new WeakMap();
|
|
2214
2922
|
const DEFAULT_GETTER = (e) => e.detail;
|
|
2215
|
-
function getHandlerKey
|
|
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
|
|
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 =
|
|
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
|
|
2231
|
-
if (
|
|
2232
|
-
|
|
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
|
|
2250
|
-
if (
|
|
2251
|
-
|
|
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
|
-
|
|
2290
|
-
|
|
2291
|
-
state[
|
|
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
|
|
3083
|
+
const registry = getCustomElementRegistry();
|
|
3084
|
+
const customClass = registry?.get(customTagName);
|
|
2299
3085
|
if (typeof customClass === "undefined") {
|
|
2300
|
-
|
|
2301
|
-
|
|
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
|
|
3093
|
+
const eventName = getEventName(binding);
|
|
2308
3094
|
const valueGetter = getValueGetter(binding);
|
|
2309
|
-
const key = getHandlerKey
|
|
2310
|
-
let twowayEventHandler = handlerByHandlerKey
|
|
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
|
|
3099
|
+
handlerByHandlerKey.set(key, twowayEventHandler);
|
|
2314
3100
|
}
|
|
2315
3101
|
binding.node.addEventListener(eventName, twowayEventHandler);
|
|
2316
|
-
bindingRegistry
|
|
3102
|
+
bindingRegistry.add(key, binding);
|
|
2317
3103
|
}
|
|
2318
3104
|
}
|
|
2319
|
-
|
|
2320
|
-
const
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
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
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
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
|
-
|
|
2356
|
-
|
|
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
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
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
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
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
|
|
2376
|
-
|
|
2377
|
-
|
|
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
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
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
|
-
|
|
2385
|
-
|
|
3202
|
+
if (modifierKey !== key)
|
|
3203
|
+
continue;
|
|
3204
|
+
if (result !== null) {
|
|
3205
|
+
raiseError(`Binding modifier "${key}" may only be specified once.`);
|
|
2386
3206
|
}
|
|
2387
|
-
|
|
3207
|
+
result = value;
|
|
2388
3208
|
}
|
|
3209
|
+
return result;
|
|
2389
3210
|
}
|
|
2390
|
-
function
|
|
2391
|
-
if (
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
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
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
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
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
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
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
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
|
-
|
|
2422
|
-
|
|
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
|
-
|
|
2426
|
-
|
|
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
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
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
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
return
|
|
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
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
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
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
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
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
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
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
const
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
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
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
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
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
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
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
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
|
-
|
|
2486
|
-
|
|
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
|
-
|
|
2491
|
-
|
|
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
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
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
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
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
|
-
|
|
2516
|
-
absoluteStateAddress =
|
|
2517
|
-
createAbsoluteStateAddress(absolutePathInfo, listIndex);
|
|
2518
|
-
absoluteStateAddressByBinding.set(binding, absoluteStateAddress);
|
|
2519
|
-
return absoluteStateAddress;
|
|
3864
|
+
return session;
|
|
2520
3865
|
}
|
|
2521
|
-
function
|
|
2522
|
-
|
|
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 =
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
3222
|
-
|
|
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
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
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 =
|
|
4994
|
+
const customClass = getCustomElementRegistry()?.get(customTagName);
|
|
3690
4995
|
if (typeof customClass === "undefined") {
|
|
3691
4996
|
return null;
|
|
3692
4997
|
}
|
|
3693
|
-
const
|
|
3694
|
-
if (
|
|
3695
|
-
return
|
|
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
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
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
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
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
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
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
|
-
|
|
4013
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5385
|
+
scheduledBindings.delete(binding);
|
|
5386
|
+
return;
|
|
4030
5387
|
}
|
|
4031
|
-
|
|
4032
|
-
})
|
|
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 (
|
|
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 は
|
|
5612
|
+
// applyChangeToProperty は propagationContextByBinding 以外の context を
|
|
5613
|
+
// 参照しないため、遅延分は最小 context を渡す
|
|
4253
5614
|
for (const { binding, value } of deferredSelectBindings) {
|
|
4254
|
-
applyChangeToProperty(binding,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
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
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
|
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
|
-
|
|
5280
|
-
|
|
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
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
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
|
|
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
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
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
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
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
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
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
|
-
|
|
6901
|
+
_queueUpdateRecords = [];
|
|
5730
6902
|
constructor() {
|
|
5731
6903
|
}
|
|
5732
|
-
enqueueAbsoluteAddress(absoluteAddress) {
|
|
5733
|
-
const requireStartProcess = this.
|
|
5734
|
-
this.
|
|
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
|
|
5738
|
-
this.
|
|
5739
|
-
this._applyChange(
|
|
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(
|
|
6922
|
+
_applyChange(updateRecords) {
|
|
5748
6923
|
// Note: AbsoluteStateAddress はキャッシュされているため、
|
|
5749
6924
|
// 同一の (stateName, address) は同じインスタンスとなり、
|
|
5750
|
-
// Set
|
|
5751
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|