@wcstack/state 2.1.0 → 2.2.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 +149 -19
- package/README.md +153 -21
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.d.ts +63 -2
- package/dist/index.esm.js +607 -24
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.esm.js +2 -0
- package/dist/wcs-manifest.json +1 -0
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -220,6 +220,7 @@ const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
|
|
220
220
|
const STATE_CONNECTED_CALLBACK_NAME = "$connectedCallback";
|
|
221
221
|
const STATE_DISCONNECTED_CALLBACK_NAME = "$disconnectedCallback";
|
|
222
222
|
const STATE_UPDATED_CALLBACK_NAME = "$updatedCallback";
|
|
223
|
+
const STATE_ERROR_CALLBACK_NAME = "$errorCallback";
|
|
223
224
|
const WEBCOMPONENT_STATE_READY_CALLBACK_NAME = "$stateReadyCallback";
|
|
224
225
|
const STATE_BINDABLES_NAME = "$bindables";
|
|
225
226
|
const STATE_COMMANDS_NAME = "$commands";
|
|
@@ -2508,6 +2509,7 @@ function buildMountRecord(component, stateProp, bindings, parentStateElement, st
|
|
|
2508
2509
|
accessorBySuffixByMarkerParent: new Map(),
|
|
2509
2510
|
indexShiftByLoopElementPath: new Map(),
|
|
2510
2511
|
addedGetterPaths: new Set(),
|
|
2512
|
+
exports: new Map(),
|
|
2511
2513
|
};
|
|
2512
2514
|
}
|
|
2513
2515
|
function firstSegmentOf(path) {
|
|
@@ -2973,6 +2975,7 @@ const setByAddressSymbol = Symbol("$$setByAddress");
|
|
|
2973
2975
|
const connectedCallbackSymbol = Symbol("$$connectedCallback");
|
|
2974
2976
|
const disconnectedCallbackSymbol = Symbol("$$disconnectedCallback");
|
|
2975
2977
|
const updatedCallbackSymbol = Symbol("$$updatedCallback");
|
|
2978
|
+
const errorCallbackSymbol = Symbol("$$errorCallback");
|
|
2976
2979
|
|
|
2977
2980
|
const _cache$3 = new WeakMap();
|
|
2978
2981
|
function getAbsolutePathInfo(stateElement, pathInfo) {
|
|
@@ -4225,6 +4228,46 @@ const handlerByHandlerKey = new Map();
|
|
|
4225
4228
|
const bindingRegistry = createHandlerBindingRegistry();
|
|
4226
4229
|
const producerValueObserversByNode = new WeakMap();
|
|
4227
4230
|
const DEFAULT_GETTER = (e) => e.detail;
|
|
4231
|
+
/**
|
|
4232
|
+
* 既定 getter(`(e) => e.detail`)が要素の宣言と噛み合っていない典型 2 形を、
|
|
4233
|
+
* 要素 × プロパティごとに 1 回だけ警告する(README「What the element writes back」)。
|
|
4234
|
+
*
|
|
4235
|
+
* (a) detail が undefined なのに `element[propName]` には値がある —
|
|
4236
|
+
* CustomEvent でない Event を dispatch している / `detail` を付け忘れている
|
|
4237
|
+
* (b) detail が `{ <propName>: … }` の形のラッパーで、`element[propName]` はオブジェクトでない —
|
|
4238
|
+
* `getter: (e) => e.detail.<propName>` が要る
|
|
4239
|
+
*
|
|
4240
|
+
* どちらも state には黙って undefined / ラッパーが書かれ、例外も lint 診断も出ない
|
|
4241
|
+
* (payload の形は静的に見えない)。挙動は変えない — 書き込みはそのまま行う。
|
|
4242
|
+
* occurrence(`semantics: "event"`)は payload が任意なので対象外(呼び出し側で除外)。
|
|
4243
|
+
*/
|
|
4244
|
+
const warnedDefaultGetter = new WeakMap();
|
|
4245
|
+
function warnDefaultGetterMismatch(node, propName, detail) {
|
|
4246
|
+
const propValue = node[propName];
|
|
4247
|
+
let reason = null;
|
|
4248
|
+
if (typeof detail === "undefined") {
|
|
4249
|
+
if (typeof propValue !== "undefined") {
|
|
4250
|
+
reason = `the event carried no detail (undefined) while element.${propName} is ${typeof propValue}`;
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
else if (detail !== null && typeof detail === "object" && Object.prototype.hasOwnProperty.call(detail, propName)
|
|
4254
|
+
&& (propValue === null || typeof propValue !== "object")) {
|
|
4255
|
+
reason = `the event's detail is an object with a "${propName}" key while element.${propName} is ${typeof propValue}`;
|
|
4256
|
+
}
|
|
4257
|
+
if (reason === null)
|
|
4258
|
+
return;
|
|
4259
|
+
let props = warnedDefaultGetter.get(node);
|
|
4260
|
+
if (typeof props === "undefined") {
|
|
4261
|
+
props = new Set();
|
|
4262
|
+
warnedDefaultGetter.set(node, props);
|
|
4263
|
+
}
|
|
4264
|
+
if (props.has(propName))
|
|
4265
|
+
return;
|
|
4266
|
+
props.add(propName);
|
|
4267
|
+
console.warn(`[@wcstack/state] [wcs/default-getter-mismatch] <${node.tagName.toLowerCase()}> "${propName}": ${reason}. ` +
|
|
4268
|
+
`With no getter, state receives e.detail as-is. Dispatch the value itself as detail, or declare ` +
|
|
4269
|
+
`getter (e.g. (e) => e.detail.${propName}, or (e) => e.target.${propName}) on that wcBindable property.`);
|
|
4270
|
+
}
|
|
4228
4271
|
function getHandlerKey(binding, eventName, hasGetter, isOccurrence) {
|
|
4229
4272
|
const filterKey = binding.inFilters.map(f => f.filterName + '(' + f.args.join(',') + ')').join('|');
|
|
4230
4273
|
return `${binding.propName}::${binding.statePathName}::${eventName}::${filterKey}::${hasGetter ? 'g' : 'n'}::${isOccurrence ? 'o' : 's'}`;
|
|
@@ -4285,6 +4328,9 @@ const twowayEventHandlerFunction = (propName, statePathName, inFilters, valueGet
|
|
|
4285
4328
|
let newValue;
|
|
4286
4329
|
if (valueGetter !== null) {
|
|
4287
4330
|
newValue = valueGetter(event);
|
|
4331
|
+
if (valueGetter === DEFAULT_GETTER && !isOccurrence) {
|
|
4332
|
+
warnDefaultGetterMismatch(node, propName, newValue);
|
|
4333
|
+
}
|
|
4288
4334
|
}
|
|
4289
4335
|
else {
|
|
4290
4336
|
if (!(propName in node)) {
|
|
@@ -7581,6 +7627,113 @@ function clearSsrPropertyStore() {
|
|
|
7581
7627
|
trackedNodes.clear();
|
|
7582
7628
|
}
|
|
7583
7629
|
|
|
7630
|
+
/**
|
|
7631
|
+
* Trusted Types (`require-trusted-types-for 'script'`) 対応。正本は docs/csp.md §7。
|
|
7632
|
+
*
|
|
7633
|
+
* state が HTML sink に流すのは **状態の値**(`innerHTML: path` などのプロパティ
|
|
7634
|
+
* バインド)で、ユーザー入力が混ざり得る文字列そのもの。ここに identity policy を
|
|
7635
|
+
* 噛ませて通すのは TT の無効化と同義なので、state は自前の policy を作らない。
|
|
7636
|
+
* 利用側が sanitizer を持つ policy を注入したときだけ通し、無ければ従来どおり
|
|
7637
|
+
* ブラウザに弾かせる(ただし何を設定すれば直るかは必ず言う)。
|
|
7638
|
+
*
|
|
7639
|
+
* 注入口は全 @wcstack パッケージ共通のグローバルスロット。buildless(CDN 一発)でも
|
|
7640
|
+
* inline script 1 本で差し込める:
|
|
7641
|
+
*
|
|
7642
|
+
* ```js
|
|
7643
|
+
* globalThis[Symbol.for("wcstack.trustedTypes.policy")] =
|
|
7644
|
+
* trustedTypes.createPolicy("my-app", { createHTML: (s) => DOMPurify.sanitize(s) });
|
|
7645
|
+
* ```
|
|
7646
|
+
*
|
|
7647
|
+
* バンドラ経由なら `setTrustedTypesPolicy()` を使う。値は毎回スロットから読むので
|
|
7648
|
+
* 後から差し替えても効く(identity policy を作る router / worker 側だけは
|
|
7649
|
+
* `createPolicy` の重複を避けるため生成結果をシングルトンで保持する)。
|
|
7650
|
+
*/
|
|
7651
|
+
/** 利用側が policy を差し込むグローバルスロット(全 @wcstack パッケージ共通)。 */
|
|
7652
|
+
const TRUSTED_TYPES_POLICY_SLOT = Symbol.for("wcstack.trustedTypes.policy");
|
|
7653
|
+
/**
|
|
7654
|
+
* 利用側が注入した policy を返す。state はここに identity policy をフォールバック
|
|
7655
|
+
* させない(それをやると TT を無効化することになる)。
|
|
7656
|
+
*/
|
|
7657
|
+
function getTrustedTypesPolicy() {
|
|
7658
|
+
const value = globalThis[TRUSTED_TYPES_POLICY_SLOT];
|
|
7659
|
+
if (value === null || typeof value !== "object")
|
|
7660
|
+
return null;
|
|
7661
|
+
return value;
|
|
7662
|
+
}
|
|
7663
|
+
/** 利用側 policy を設定する(`null` で解除)。最初のバインド適用前に呼ぶこと。 */
|
|
7664
|
+
function setTrustedTypesPolicy(policy) {
|
|
7665
|
+
globalThis[TRUSTED_TYPES_POLICY_SLOT] = policy;
|
|
7666
|
+
}
|
|
7667
|
+
/**
|
|
7668
|
+
* TrustedHTML が要求されるプロパティか。`textContent` などの安全な sink は含めない。
|
|
7669
|
+
* ホットパス(全プロパティ書き込み)から呼ばれるので文字列比較だけで済ませる。
|
|
7670
|
+
*/
|
|
7671
|
+
function isHtmlSinkProp(prop) {
|
|
7672
|
+
return prop === "innerHTML" || prop === "outerHTML" || prop === "srcdoc";
|
|
7673
|
+
}
|
|
7674
|
+
/**
|
|
7675
|
+
* HTML sink へ書く値を利用側 policy に通す。policy が無ければ値をそのまま返す
|
|
7676
|
+
* = TT 有効下ではブラウザが弾く(意図どおり)。policy がある場合は TT 非対応
|
|
7677
|
+
* ブラウザでも通す: sanitizer は Chromium だけで効いても意味がないため。
|
|
7678
|
+
*/
|
|
7679
|
+
function trustHtmlValue(value) {
|
|
7680
|
+
if (typeof value !== "string")
|
|
7681
|
+
return value;
|
|
7682
|
+
const policy = getTrustedTypesPolicy();
|
|
7683
|
+
const createHTML = policy?.createHTML;
|
|
7684
|
+
if (typeof createHTML !== "function")
|
|
7685
|
+
return value;
|
|
7686
|
+
return createHTML.call(policy, value);
|
|
7687
|
+
}
|
|
7688
|
+
let _enforced = undefined;
|
|
7689
|
+
/**
|
|
7690
|
+
* TT が実際に強制されているかを実測する。エラーメッセージの文言に依存しないよう、
|
|
7691
|
+
* 使い捨ての要素へ実際に書いて確かめる。`default` policy がある場合は書き込みが
|
|
7692
|
+
* 通る=我々の書き込みも通るので、正しく false になる。
|
|
7693
|
+
*
|
|
7694
|
+
* cold path(書き込みが失敗した後)でしか呼ばれない。
|
|
7695
|
+
*/
|
|
7696
|
+
function isTrustedTypesEnforced() {
|
|
7697
|
+
if (_enforced !== undefined)
|
|
7698
|
+
return _enforced;
|
|
7699
|
+
if (!("trustedTypes" in globalThis)) {
|
|
7700
|
+
_enforced = false;
|
|
7701
|
+
return _enforced;
|
|
7702
|
+
}
|
|
7703
|
+
try {
|
|
7704
|
+
document.createElement("div").innerHTML = "<i></i>";
|
|
7705
|
+
_enforced = false;
|
|
7706
|
+
}
|
|
7707
|
+
catch {
|
|
7708
|
+
_enforced = true;
|
|
7709
|
+
}
|
|
7710
|
+
return _enforced;
|
|
7711
|
+
}
|
|
7712
|
+
let _reported = false;
|
|
7713
|
+
/**
|
|
7714
|
+
* HTML sink への書き込み失敗を診断する。applyChangeToProperty の catch は
|
|
7715
|
+
* `config.debug` 時しか warn しないため、TT が原因のときは黙って壊れていた。
|
|
7716
|
+
* 原因と直し方が分かる形で一度だけ報告する。
|
|
7717
|
+
*/
|
|
7718
|
+
function reportTrustedTypesBlock(element, prop) {
|
|
7719
|
+
if (_reported)
|
|
7720
|
+
return;
|
|
7721
|
+
if (!isTrustedTypesEnforced())
|
|
7722
|
+
return;
|
|
7723
|
+
_reported = true;
|
|
7724
|
+
const hasPolicy = typeof getTrustedTypesPolicy()?.createHTML === "function";
|
|
7725
|
+
const cause = hasPolicy
|
|
7726
|
+
? "The injected policy's createHTML() did not return a TrustedHTML."
|
|
7727
|
+
: "No sanitizing policy is installed, and @wcstack/state deliberately does not "
|
|
7728
|
+
+ "pass state values through an identity policy — that would defeat the CSP.";
|
|
7729
|
+
console.error(`[@wcstack/state] Writing to "${prop}" was blocked by Trusted Types `
|
|
7730
|
+
+ `(require-trusted-types-for 'script'). ${cause}\n`
|
|
7731
|
+
+ `Install a sanitizing policy before the first binding is applied:\n`
|
|
7732
|
+
+ ` globalThis[Symbol.for("wcstack.trustedTypes.policy")] =\n`
|
|
7733
|
+
+ ` trustedTypes.createPolicy("my-app", { createHTML: (s) => DOMPurify.sanitize(s) });\n`
|
|
7734
|
+
+ `Or bind the value as text instead of HTML. See docs/csp.md section 7.`, { element, property: prop });
|
|
7735
|
+
}
|
|
7736
|
+
|
|
7584
7737
|
// SSR 時に HTML 属性で代替可能なプロパティ
|
|
7585
7738
|
// これら以外のプロパティは ssrPropertyStore に蓄積してハイドレーション時に復元
|
|
7586
7739
|
const SSR_ATTR_PROPS = {
|
|
@@ -7652,13 +7805,23 @@ function applyChangeToProperty(binding, _context, newValue) {
|
|
|
7652
7805
|
&& getCustomElement(element) !== null) {
|
|
7653
7806
|
rememberOverwrittenObject(element, firstSegment, current);
|
|
7654
7807
|
}
|
|
7808
|
+
// Trusted Types: HTML sink (`innerHTML` 等) への書き込みだけ、利用側が注入した
|
|
7809
|
+
// sanitizer 付き policy を通す。state が identity policy を作って素通しさせるのは
|
|
7810
|
+
// TT の無効化と同義なので採らない(docs/csp.md §7)。sink 以外は文字列比較 3 回で
|
|
7811
|
+
// 抜けるので、ホットパスの実コストはほぼ無い。
|
|
7812
|
+
const isHtmlSink = isHtmlSinkProp(firstSegment);
|
|
7655
7813
|
const performWrite = () => {
|
|
7656
7814
|
let propertyWriteSucceeded = false;
|
|
7657
7815
|
try {
|
|
7658
|
-
element[firstSegment] = newValue;
|
|
7816
|
+
element[firstSegment] = isHtmlSink ? trustHtmlValue(newValue) : newValue;
|
|
7659
7817
|
propertyWriteSucceeded = true;
|
|
7660
7818
|
}
|
|
7661
7819
|
catch (error) {
|
|
7820
|
+
// TT が原因のときは config.debug に関係なく報告する。ここを黙って握り潰すと
|
|
7821
|
+
// 「バインドを書いたのに何も起きない」という最悪の壊れ方をする。
|
|
7822
|
+
if (isHtmlSink) {
|
|
7823
|
+
reportTrustedTypesBlock(element, firstSegment);
|
|
7824
|
+
}
|
|
7662
7825
|
if (config.debug) {
|
|
7663
7826
|
console.warn(`Failed to set property '${firstSegment}' on element.`, {
|
|
7664
7827
|
element,
|
|
@@ -8275,6 +8438,61 @@ function checkDeclaredPath(stateElement, state, path, source) {
|
|
|
8275
8438
|
if (result.existence !== "missing") {
|
|
8276
8439
|
return;
|
|
8277
8440
|
}
|
|
8441
|
+
if (isExportedPath(stateElement, path)) {
|
|
8442
|
+
return;
|
|
8443
|
+
}
|
|
8444
|
+
if (source === "binding") {
|
|
8445
|
+
// 遅延報告(docs/state-overlay-export-design.md X7): バインド確立時点では、その位置に
|
|
8446
|
+
// マウントされるコンポーネントの getter(公開 getter)がまだ登録されていない。
|
|
8447
|
+
// 1 マクロタスク待って、登録で解消しなかったものだけを報告する
|
|
8448
|
+
deferReport(stateElement, path, result);
|
|
8449
|
+
return;
|
|
8450
|
+
}
|
|
8451
|
+
reportMissing(stateElement, path, source, result);
|
|
8452
|
+
}
|
|
8453
|
+
const deferredReportsByStateElement = new WeakMap();
|
|
8454
|
+
const flushScheduled = new WeakSet();
|
|
8455
|
+
const exportedPathsByStateElement = new WeakMap();
|
|
8456
|
+
/** 公開 getter の登録(webComponent/exportIndex.ts)— このパスは「存在しない」ではない */
|
|
8457
|
+
function markExportedPath(stateElement, path) {
|
|
8458
|
+
let paths = exportedPathsByStateElement.get(stateElement);
|
|
8459
|
+
if (typeof paths === "undefined") {
|
|
8460
|
+
paths = new Set();
|
|
8461
|
+
exportedPathsByStateElement.set(stateElement, paths);
|
|
8462
|
+
}
|
|
8463
|
+
paths.add(path);
|
|
8464
|
+
deferredReportsByStateElement.get(stateElement)?.delete(path);
|
|
8465
|
+
}
|
|
8466
|
+
function isExportedPath(stateElement, path) {
|
|
8467
|
+
return exportedPathsByStateElement.get(stateElement)?.has(path) === true;
|
|
8468
|
+
}
|
|
8469
|
+
function deferReport(stateElement, path, result) {
|
|
8470
|
+
let pending = deferredReportsByStateElement.get(stateElement);
|
|
8471
|
+
if (typeof pending === "undefined") {
|
|
8472
|
+
pending = new Map();
|
|
8473
|
+
deferredReportsByStateElement.set(stateElement, pending);
|
|
8474
|
+
}
|
|
8475
|
+
pending.set(path, result);
|
|
8476
|
+
if (flushScheduled.has(stateElement)) {
|
|
8477
|
+
return;
|
|
8478
|
+
}
|
|
8479
|
+
flushScheduled.add(stateElement);
|
|
8480
|
+
setTimeout(() => flushDeferredPathReports(stateElement), 0);
|
|
8481
|
+
}
|
|
8482
|
+
/** 遅延中の報告を今すぐ流す(タイマー到達時・テスト用) */
|
|
8483
|
+
function flushDeferredPathReports(stateElement) {
|
|
8484
|
+
flushScheduled.delete(stateElement);
|
|
8485
|
+
const pending = deferredReportsByStateElement.get(stateElement);
|
|
8486
|
+
if (typeof pending === "undefined") {
|
|
8487
|
+
return;
|
|
8488
|
+
}
|
|
8489
|
+
deferredReportsByStateElement.delete(stateElement);
|
|
8490
|
+
// 登録で解消したものは markExportedPath が pending から消している
|
|
8491
|
+
for (const [path, result] of pending) {
|
|
8492
|
+
reportMissing(stateElement, path, "binding", result);
|
|
8493
|
+
}
|
|
8494
|
+
}
|
|
8495
|
+
function reportMissing(stateElement, path, source, result) {
|
|
8278
8496
|
// 接頭辞は raiseError と同じ `[@wcstack/state] [wcs/...]` の並び(コンソールの
|
|
8279
8497
|
// grep 単位をパッケージで揃える)
|
|
8280
8498
|
console.warn(`[@wcstack/state] [${DIAGNOSTIC_CODE[source]}] ${SUBJECT[source]} "${path}" does not resolve on the state tree: ` +
|
|
@@ -8435,7 +8653,7 @@ function _applyChange(binding, context) {
|
|
|
8435
8653
|
const value = getValue(context.state, binding);
|
|
8436
8654
|
const filteredValue = getFilteredValue(value, binding.outFilters);
|
|
8437
8655
|
if (deferredSelectBindingByBinding.get(binding) === true) {
|
|
8438
|
-
context.deferredSelectBindings.push({ binding, value: filteredValue });
|
|
8656
|
+
context.deferredSelectBindings.push({ binding, value: filteredValue, stateElement: context.stateElement });
|
|
8439
8657
|
return;
|
|
8440
8658
|
}
|
|
8441
8659
|
let fn = fnByBinding.get(binding);
|
|
@@ -8475,7 +8693,7 @@ function _applyChange(binding, context) {
|
|
|
8475
8693
|
if (element.tagName === 'SELECT') {
|
|
8476
8694
|
const propName = binding.propSegments[0];
|
|
8477
8695
|
if (propName === 'value' || propName === 'selectedIndex') {
|
|
8478
|
-
context.deferredSelectBindings.push({ binding, value: filteredValue });
|
|
8696
|
+
context.deferredSelectBindings.push({ binding, value: filteredValue, stateElement: context.stateElement });
|
|
8479
8697
|
deferredSelectBindingByBinding.set(binding, true);
|
|
8480
8698
|
return;
|
|
8481
8699
|
}
|
|
@@ -8569,10 +8787,31 @@ function applyChange(binding, context) {
|
|
|
8569
8787
|
* `console.error` だけだと devtools からは「静かに握られた失敗」が見えないため、
|
|
8570
8788
|
* 同じ地点から sink にも流す(`state:watch-error` と同じ位置づけ)。
|
|
8571
8789
|
* 値と DOM は巻き戻さない — 伝播 hop 上限超過・watch 連鎖打ち切りと同じ姿勢。
|
|
8790
|
+
*
|
|
8791
|
+
* state が `$errorCallback` を宣言していれば、console.error の代わりにそこへ配送する
|
|
8792
|
+
* (作者が報告を引き取った。ページ内で受けるための口)。配送は batch の末尾 —
|
|
8793
|
+
* $updatedCallback と同じ位置 — にまとめる。devtools sink へは宣言の有無に関わらず流す。
|
|
8572
8794
|
*/
|
|
8573
|
-
function reportBindingApplyError(binding, error) {
|
|
8574
|
-
|
|
8575
|
-
|
|
8795
|
+
function reportBindingApplyError(binding, error, stateElement, failuresByStateElement) {
|
|
8796
|
+
const handled = stateElement !== null && stateElement.hasErrorCallback === true;
|
|
8797
|
+
if (handled) {
|
|
8798
|
+
const info = {
|
|
8799
|
+
path: binding.statePathName,
|
|
8800
|
+
bindingType: binding.bindingType,
|
|
8801
|
+
node: binding.node,
|
|
8802
|
+
};
|
|
8803
|
+
const failures = failuresByStateElement.get(stateElement);
|
|
8804
|
+
if (failures === undefined) {
|
|
8805
|
+
failuresByStateElement.set(stateElement, [{ error, info }]);
|
|
8806
|
+
}
|
|
8807
|
+
else {
|
|
8808
|
+
failures.push({ error, info });
|
|
8809
|
+
}
|
|
8810
|
+
}
|
|
8811
|
+
else {
|
|
8812
|
+
console.error(`[@wcstack/state] binding "${binding.bindingType}: ${binding.statePathName}" failed to apply; ` +
|
|
8813
|
+
`the rest of this batch continues.`, { node: binding.node, error });
|
|
8814
|
+
}
|
|
8576
8815
|
if (devtoolsSink !== null) {
|
|
8577
8816
|
devtoolsSink({
|
|
8578
8817
|
type: "state:binding-apply-error",
|
|
@@ -8598,6 +8837,7 @@ function applyChangeFromBindings(bindings, propagationContextByBinding) {
|
|
|
8598
8837
|
const newListValueByAbsAddress = new Map();
|
|
8599
8838
|
const updatedAbsAddressSetByStateElement = new Map();
|
|
8600
8839
|
const deferredSelectBindings = [];
|
|
8840
|
+
const failuresByStateElement = new Map();
|
|
8601
8841
|
// Phase 1: 構造的更新 + 値更新(select.value/selectedIndex は遅延)
|
|
8602
8842
|
while (bindingIndex < bindings.length) {
|
|
8603
8843
|
let binding = bindings[bindingIndex];
|
|
@@ -8643,7 +8883,7 @@ function applyChangeFromBindings(bindings, propagationContextByBinding) {
|
|
|
8643
8883
|
applyChange(binding, context);
|
|
8644
8884
|
}
|
|
8645
8885
|
catch (error) {
|
|
8646
|
-
reportBindingApplyError(binding, error);
|
|
8886
|
+
reportBindingApplyError(binding, error, stateElement, failuresByStateElement);
|
|
8647
8887
|
}
|
|
8648
8888
|
bindingIndex++;
|
|
8649
8889
|
const nextBindingInfo = bindings[bindingIndex];
|
|
@@ -8659,12 +8899,12 @@ function applyChangeFromBindings(bindings, propagationContextByBinding) {
|
|
|
8659
8899
|
// Phase 2: 遅延されたselect.value/selectedIndex を適用
|
|
8660
8900
|
// applyChangeToProperty は propagationContextByBinding 以外の context を
|
|
8661
8901
|
// 参照しないため、遅延分は最小 context を渡す
|
|
8662
|
-
for (const { binding, value } of deferredSelectBindings) {
|
|
8902
|
+
for (const { binding, value, stateElement } of deferredSelectBindings) {
|
|
8663
8903
|
try {
|
|
8664
8904
|
applyChangeToProperty(binding, { propagationContextByBinding }, value);
|
|
8665
8905
|
}
|
|
8666
8906
|
catch (error) {
|
|
8667
|
-
reportBindingApplyError(binding, error);
|
|
8907
|
+
reportBindingApplyError(binding, error, stateElement ?? null, failuresByStateElement);
|
|
8668
8908
|
}
|
|
8669
8909
|
}
|
|
8670
8910
|
for (const [absAddress, newListValue] of newListValueByAbsAddress.entries()) {
|
|
@@ -8675,6 +8915,20 @@ function applyChangeFromBindings(bindings, propagationContextByBinding) {
|
|
|
8675
8915
|
state[updatedCallbackSymbol](Array.from(absAddressSet));
|
|
8676
8916
|
});
|
|
8677
8917
|
}
|
|
8918
|
+
// $errorCallback の配送。$updatedCallback の後・失敗した本数ぶん・this は writable proxy。
|
|
8919
|
+
// callback 自身の throw は隔離する — 1 件の報告失敗が残りの報告と drain を道連れにしない
|
|
8920
|
+
for (const [stateElement, failures] of failuresByStateElement.entries()) {
|
|
8921
|
+
stateElement.createState("writable", (state) => {
|
|
8922
|
+
for (const { error, info } of failures) {
|
|
8923
|
+
try {
|
|
8924
|
+
state[errorCallbackSymbol](error, info);
|
|
8925
|
+
}
|
|
8926
|
+
catch (callbackError) {
|
|
8927
|
+
console.error(`[@wcstack/state] $errorCallback threw while handling the failure of binding "${info.bindingType}: ${info.path}".`, { error: callbackError, original: error, node: info.node });
|
|
8928
|
+
}
|
|
8929
|
+
}
|
|
8930
|
+
});
|
|
8931
|
+
}
|
|
8678
8932
|
}
|
|
8679
8933
|
|
|
8680
8934
|
function scheduleDeferredSpreads(deferredSpreads, parentLoopContext, session) {
|
|
@@ -9182,7 +9436,7 @@ async function buildBindings(root) {
|
|
|
9182
9436
|
}
|
|
9183
9437
|
}
|
|
9184
9438
|
|
|
9185
|
-
var version = "2.
|
|
9439
|
+
var version = "2.2.0";
|
|
9186
9440
|
var pkg = {
|
|
9187
9441
|
version: version};
|
|
9188
9442
|
|
|
@@ -10856,6 +11110,7 @@ function registerDevtoolsSource() {
|
|
|
10856
11110
|
delta: record.delta,
|
|
10857
11111
|
privateKeys: Object.keys(record.privateSnapshot),
|
|
10858
11112
|
getterKeys: [...record.getterKeys],
|
|
11113
|
+
exports: [...record.exports.keys()],
|
|
10859
11114
|
}));
|
|
10860
11115
|
},
|
|
10861
11116
|
keys(rootNode) {
|
|
@@ -13073,9 +13328,32 @@ function defineDCC(hostElement, shadowRoot, state) {
|
|
|
13073
13328
|
// 一意性はレジストリ単位なので、別スコープの同名 DCC は衝突しない。
|
|
13074
13329
|
raiseError(`DCC: "${tagName}" is already registered. A custom element name can only be defined once.`);
|
|
13075
13330
|
}
|
|
13076
|
-
// ShadowRoot
|
|
13331
|
+
// ShadowRoot 自体は cloneNode 不可なので、子ノードを 1 つずつ template へ取り込む。
|
|
13332
|
+
//
|
|
13333
|
+
// かつては `template.innerHTML = shadowRoot.innerHTML` と serialize → parse で
|
|
13334
|
+
// 往復していた。これをやめたのは 3 点の理由による(docs/csp.md §7):
|
|
13335
|
+
// (1) `require-trusted-types-for 'script'` 下では innerHTML sink が弾かれる。
|
|
13336
|
+
// ここはテンプレート=作者が書いた DOM の複製でしかないので、policy を作って
|
|
13337
|
+
// 署名するより sink 自体を無くすほうが筋が良い(state が CSP の
|
|
13338
|
+
// `trusted-types` allowlist を要求しなくなる)。
|
|
13339
|
+
// (2) 往復のたびに HTML パーサの再解釈が挟まり、元の DOM と一致しない結果に
|
|
13340
|
+
// なり得る(mXSS と同じ機序)。
|
|
13341
|
+
// (3) 単純に serialize + parse のぶん遅い。
|
|
13342
|
+
//
|
|
13343
|
+
// importNode は **取り込み先 document** で要素を作るため、template の inert な
|
|
13344
|
+
// contents document 側から呼べば従来どおり「未 upgrade の複製」になる。live
|
|
13345
|
+
// document 側で cloneNode すると upgrade reaction が走り、_ensureShadow の明示
|
|
13346
|
+
// upgrade と二重になる。
|
|
13347
|
+
//
|
|
13348
|
+
// 挙動差が 1 つある: script 要素の already-started フラグは複製時に引き継がれる
|
|
13349
|
+
// ため、テンプレート内のインライン `<script>` はインスタンス生成のたびにネイティブ
|
|
13350
|
+
// 実行されなくなる。`<wcs-state>` の状態定義スクリプトは text を読んで評価する実装
|
|
13351
|
+
// (loadFromInnerScript)なので影響を受けない。
|
|
13077
13352
|
const template = document.createElement("template");
|
|
13078
|
-
|
|
13353
|
+
const inertDocument = template.content.ownerDocument;
|
|
13354
|
+
for (const childNode of Array.from(shadowRoot.childNodes)) {
|
|
13355
|
+
template.content.appendChild(inertDocument.importNode(childNode, true));
|
|
13356
|
+
}
|
|
13079
13357
|
const shadowRootMode = shadowRoot.mode;
|
|
13080
13358
|
// $bindables / $commands から wcBindable + bindableEventMap を生成
|
|
13081
13359
|
const { bindables, commands, streamBackedBindables } = processDccDeclarations(state);
|
|
@@ -13620,6 +13898,22 @@ function createOverlayValue(record, address, receiver, handler) {
|
|
|
13620
13898
|
const privateData = isBase ? getPrivateData(record, address.listIndex) : {};
|
|
13621
13899
|
return new Proxy(privateData, new OverlayValueHandler(record, markerParentPath, address.listIndex, isBase, receiver, handler));
|
|
13622
13900
|
}
|
|
13901
|
+
/**
|
|
13902
|
+
* 公開 getter の読み(docs/state-overlay-export-design.md §2-1 の 4)。
|
|
13903
|
+
* `P.#m<id>` のオーバーレイ値に対する `Reflect.get(proxy, k)` と等価 — 作者の getter は
|
|
13904
|
+
* マーカーアドレスを push して評価されるので、依存辺・キャッシュはマーカー側に載る。
|
|
13905
|
+
*/
|
|
13906
|
+
function readExportedAccessor(record, entry, listIndex, receiver, handler) {
|
|
13907
|
+
const address = createStateAddress(getPathInfo(entry.markerTerminalPath), listIndex);
|
|
13908
|
+
const proxy = createOverlayValue(record, address, receiver, handler);
|
|
13909
|
+
return Reflect.get(proxy, entry.suffix);
|
|
13910
|
+
}
|
|
13911
|
+
/** 公開 getter への書き込み(X9): setter があれば評価、無ければ overlay の set が raise する。 */
|
|
13912
|
+
function writeExportedAccessor(record, entry, listIndex, value, receiver, handler) {
|
|
13913
|
+
const address = createStateAddress(getPathInfo(entry.markerTerminalPath), listIndex);
|
|
13914
|
+
const proxy = createOverlayValue(record, address, receiver, handler);
|
|
13915
|
+
return Reflect.set(proxy, entry.suffix, value);
|
|
13916
|
+
}
|
|
13623
13917
|
/**
|
|
13624
13918
|
* `element.state` の公開面(chroot・M13)。相対キーを変換して親の proxy を通すだけの
|
|
13625
13919
|
* 薄い翻訳で、値の解決(私有・getter・ツリー)は全て親ウォーク+オーバーレイが担う。
|
|
@@ -13705,6 +13999,203 @@ function createPublicMountState(record) {
|
|
|
13705
13999
|
});
|
|
13706
14000
|
}
|
|
13707
14001
|
|
|
14002
|
+
const exportIndexByStateElement = new WeakMap();
|
|
14003
|
+
const reportedShadows = new Set();
|
|
14004
|
+
function slotFor(stateElement, parentPath, key, create) {
|
|
14005
|
+
let byParent = exportIndexByStateElement.get(stateElement);
|
|
14006
|
+
if (typeof byParent === "undefined") {
|
|
14007
|
+
if (!create)
|
|
14008
|
+
return null;
|
|
14009
|
+
byParent = new Map();
|
|
14010
|
+
exportIndexByStateElement.set(stateElement, byParent);
|
|
14011
|
+
}
|
|
14012
|
+
let byKey = byParent.get(parentPath);
|
|
14013
|
+
if (typeof byKey === "undefined") {
|
|
14014
|
+
if (!create)
|
|
14015
|
+
return null;
|
|
14016
|
+
byKey = new Map();
|
|
14017
|
+
byParent.set(parentPath, byKey);
|
|
14018
|
+
}
|
|
14019
|
+
let slot = byKey.get(key);
|
|
14020
|
+
if (typeof slot === "undefined") {
|
|
14021
|
+
if (!create)
|
|
14022
|
+
return null;
|
|
14023
|
+
slot = { holders: new Set(), byListIndex: new WeakMap(), noIndex: null };
|
|
14024
|
+
byKey.set(key, slot);
|
|
14025
|
+
}
|
|
14026
|
+
return slot;
|
|
14027
|
+
}
|
|
14028
|
+
/**
|
|
14029
|
+
* 記録の getter / setter を公開索引に載せる(初回登録で 1 回・冪等)。
|
|
14030
|
+
* translateInnerPath のマーカー化を通すので accessorBySuffixByMarkerParent も同時に埋まる。
|
|
14031
|
+
* 翻訳できないアクセサ(ワイルドカード終端・部分マウントのみで接頭辞不一致)と、
|
|
14032
|
+
* `$` 名前空間のアクセサ(翻訳されずマーカーが付かない)は公開しない。
|
|
14033
|
+
* ルートエントリの無い部分マウントは公開位置(ツリー上のパス)を持たないので対象外。
|
|
14034
|
+
*/
|
|
14035
|
+
function registerExports(record) {
|
|
14036
|
+
if (record.exports.size > 0 || record.rootEntry === null) {
|
|
14037
|
+
return;
|
|
14038
|
+
}
|
|
14039
|
+
const keys = new Set([...record.getterKeys, ...record.setterKeys]);
|
|
14040
|
+
for (const key of keys) {
|
|
14041
|
+
let markerPath;
|
|
14042
|
+
try {
|
|
14043
|
+
markerPath = translateInnerPath(record, key);
|
|
14044
|
+
}
|
|
14045
|
+
catch {
|
|
14046
|
+
continue;
|
|
14047
|
+
}
|
|
14048
|
+
const markerIndex = markerPath.indexOf(DELIMITER + record.marker);
|
|
14049
|
+
if (markerIndex === -1) {
|
|
14050
|
+
continue;
|
|
14051
|
+
}
|
|
14052
|
+
// `users.*.#m7.display` → 末端マーカーパス `users.*.#m7`・接尾 `display`・公開 `users.*.display`
|
|
14053
|
+
// (接尾は常に非空 — markerizeAccessorPath が空を raise 済み。公開パスはルート
|
|
14054
|
+
// エントリの外側パス+接尾なので常に 2 セグメント以上 = 親パスを持つ)
|
|
14055
|
+
const markerTerminalPath = markerPath.slice(0, markerIndex + 1 + record.marker.length);
|
|
14056
|
+
const suffix = markerPath.slice(markerTerminalPath.length + 1);
|
|
14057
|
+
const exportedPath = markerPath.slice(0, markerIndex) + DELIMITER + suffix;
|
|
14058
|
+
const exportedInfo = getPathInfo(exportedPath);
|
|
14059
|
+
// Internal wildcard accessors need their own row resolution and lifecycle
|
|
14060
|
+
// notifications. Only publish accessors at the mount instance's depth.
|
|
14061
|
+
if (exportedInfo.wildcardCount !== record.delta) {
|
|
14062
|
+
continue;
|
|
14063
|
+
}
|
|
14064
|
+
const entry = { markerTerminalPath, suffix, markerPath, exportedPath };
|
|
14065
|
+
record.exports.set(exportedPath, entry);
|
|
14066
|
+
slotFor(record.parentStateElement, exportedInfo.parentPath, exportedInfo.lastSegment, true)
|
|
14067
|
+
.holders.add({ ref: new WeakRef(record), entry });
|
|
14068
|
+
// エイリアス辺(X5): 子 getter のアドレス → 公開パス
|
|
14069
|
+
record.parentStateElement.addDynamicDependency(markerPath, exportedPath);
|
|
14070
|
+
// 未存在パスの遅延診断(X7): この公開パスへのバインドは「存在しない」ではない
|
|
14071
|
+
markExportedPath(record.parentStateElement, exportedPath);
|
|
14072
|
+
}
|
|
14073
|
+
record.parentStateElement.markHasMounts?.();
|
|
14074
|
+
}
|
|
14075
|
+
/** 読みの listIndex がホスト要素のループ文脈と一致するか(配下の深い文脈も一致とみなす) */
|
|
14076
|
+
function isInstanceOf(record, listIndex) {
|
|
14077
|
+
if (!record.component.isConnected) {
|
|
14078
|
+
return false;
|
|
14079
|
+
}
|
|
14080
|
+
const own = getLoopContextByNode(record.component)?.listIndex ?? null;
|
|
14081
|
+
if (listIndex === null) {
|
|
14082
|
+
return own === null;
|
|
14083
|
+
}
|
|
14084
|
+
let current = own;
|
|
14085
|
+
while (current !== null) {
|
|
14086
|
+
if (current === listIndex) {
|
|
14087
|
+
return true;
|
|
14088
|
+
}
|
|
14089
|
+
current = current.parentListIndex;
|
|
14090
|
+
}
|
|
14091
|
+
return false;
|
|
14092
|
+
}
|
|
14093
|
+
/** ホルダーが生きていて、この listIndex のインスタンスなら記録を返す */
|
|
14094
|
+
function liveInstance(holder, listIndex) {
|
|
14095
|
+
const record = holder.ref.deref();
|
|
14096
|
+
if (typeof record === "undefined" || !isInstanceOf(record, listIndex)) {
|
|
14097
|
+
return null;
|
|
14098
|
+
}
|
|
14099
|
+
return record;
|
|
14100
|
+
}
|
|
14101
|
+
/**
|
|
14102
|
+
* `P.k`(listIndex)に答える記録を引く。索引に無ければ null(今日どおり undefined 解決)。
|
|
14103
|
+
* 複数一致は raise。
|
|
14104
|
+
*/
|
|
14105
|
+
function resolveExport(stateElement, parentPath, key, listIndex) {
|
|
14106
|
+
const slot = slotFor(stateElement, parentPath, key, false);
|
|
14107
|
+
if (slot === null) {
|
|
14108
|
+
return null;
|
|
14109
|
+
}
|
|
14110
|
+
const cached = listIndex === null ? slot.noIndex : (slot.byListIndex.get(listIndex) ?? null);
|
|
14111
|
+
if (cached !== null) {
|
|
14112
|
+
const record = liveInstance(cached, listIndex);
|
|
14113
|
+
if (record !== null) {
|
|
14114
|
+
return { record, entry: cached.entry };
|
|
14115
|
+
}
|
|
14116
|
+
}
|
|
14117
|
+
let found = null;
|
|
14118
|
+
let foundRecord = null;
|
|
14119
|
+
for (const holder of slot.holders) {
|
|
14120
|
+
const record = holder.ref.deref();
|
|
14121
|
+
if (typeof record === "undefined") {
|
|
14122
|
+
// 記録は回収済み(finalizer 発火前の窓)— 遅延 prune
|
|
14123
|
+
slot.holders.delete(holder);
|
|
14124
|
+
continue;
|
|
14125
|
+
}
|
|
14126
|
+
if (!isInstanceOf(record, listIndex)) {
|
|
14127
|
+
continue;
|
|
14128
|
+
}
|
|
14129
|
+
if (foundRecord !== null) {
|
|
14130
|
+
raiseError(`[wcs/mount-export-ambiguous] "${parentPath}${DELIMITER}${key}" is exported by two mounted components on the same instance: ` +
|
|
14131
|
+
`<${foundRecord.component.tagName.toLowerCase()}> and <${record.component.tagName.toLowerCase()}>. ` +
|
|
14132
|
+
`Mount only one of them there, or rename one accessor. See docs/state-overlay-export-design.md X4.`);
|
|
14133
|
+
}
|
|
14134
|
+
found = holder;
|
|
14135
|
+
foundRecord = record;
|
|
14136
|
+
}
|
|
14137
|
+
if (found === null || foundRecord === null) {
|
|
14138
|
+
return null;
|
|
14139
|
+
}
|
|
14140
|
+
if (listIndex === null) {
|
|
14141
|
+
slot.noIndex = found;
|
|
14142
|
+
}
|
|
14143
|
+
else {
|
|
14144
|
+
slot.byListIndex.set(listIndex, found);
|
|
14145
|
+
}
|
|
14146
|
+
return { record: foundRecord, entry: found.entry };
|
|
14147
|
+
}
|
|
14148
|
+
/** 公開パスの `$postUpdate` を、記録のホスト要素のループ文脈で打つ(X6)。 */
|
|
14149
|
+
function notifyExports(record) {
|
|
14150
|
+
const parent = record.parentStateElement;
|
|
14151
|
+
const loopContext = getLoopContextByNode(record.component);
|
|
14152
|
+
if (parent.isConnected === false || (record.delta > 0 && loopContext === null)) {
|
|
14153
|
+
// A removed tree needs no notification. A removed row is handled by its
|
|
14154
|
+
// parent's list update. Other notification failures must remain visible.
|
|
14155
|
+
return;
|
|
14156
|
+
}
|
|
14157
|
+
for (const entry of record.exports.values()) {
|
|
14158
|
+
parent.createState("readonly", (state) => {
|
|
14159
|
+
state[setLoopContextSymbol](loopContext, () => {
|
|
14160
|
+
state.$postUpdate(entry.exportedPath);
|
|
14161
|
+
});
|
|
14162
|
+
});
|
|
14163
|
+
}
|
|
14164
|
+
}
|
|
14165
|
+
/**
|
|
14166
|
+
* X1: ツリーに同名キーがある公開 getter は親から読まれない(ツリーが勝つ)。
|
|
14167
|
+
* 登録時に 1 回 warn(タグ × 公開パス)。行マウントはホスト要素のループ文脈で読む。
|
|
14168
|
+
*/
|
|
14169
|
+
function warnShadowedExports(record) {
|
|
14170
|
+
const loopContext = getLoopContextByNode(record.component);
|
|
14171
|
+
if (record.delta > 0 && loopContext === null) {
|
|
14172
|
+
// 行マウントでループ文脈が無い(行の実体化前)— 読めないので黙る
|
|
14173
|
+
return;
|
|
14174
|
+
}
|
|
14175
|
+
const tag = record.component.tagName.toLowerCase();
|
|
14176
|
+
for (const entry of record.exports.values()) {
|
|
14177
|
+
const reportKey = `${tag}|${entry.exportedPath}`;
|
|
14178
|
+
if (reportedShadows.has(reportKey)) {
|
|
14179
|
+
continue;
|
|
14180
|
+
}
|
|
14181
|
+
const exportedInfo = getPathInfo(entry.exportedPath);
|
|
14182
|
+
let parentValue = undefined;
|
|
14183
|
+
record.parentStateElement.createState("readonly", (state) => {
|
|
14184
|
+
state[setLoopContextSymbol](loopContext, () => {
|
|
14185
|
+
parentValue = state[exportedInfo.parentPath];
|
|
14186
|
+
});
|
|
14187
|
+
});
|
|
14188
|
+
if (parentValue === null || typeof parentValue === "undefined"
|
|
14189
|
+
|| !(exportedInfo.lastSegment in Object(parentValue))) {
|
|
14190
|
+
continue;
|
|
14191
|
+
}
|
|
14192
|
+
reportedShadows.add(reportKey);
|
|
14193
|
+
console.warn(`[@wcstack/state] [wcs/mount-export-shadowed] <${tag}>.${record.stateProp}.${entry.suffix} is exported at ` +
|
|
14194
|
+
`"${entry.exportedPath}" but the tree already has that key, so readers outside the component get the tree value. ` +
|
|
14195
|
+
`Remove the tree key or rename the accessor. See docs/state-overlay-export-design.md X1.`);
|
|
14196
|
+
}
|
|
14197
|
+
}
|
|
14198
|
+
|
|
13708
14199
|
/**
|
|
13709
14200
|
* このアドレスの値をキャッシュしてよいか(getByAddress / setByAddress 共通の判定)。
|
|
13710
14201
|
*
|
|
@@ -13839,6 +14330,16 @@ function _getByAddress(target, address, receiver, handler, stateElement) {
|
|
|
13839
14330
|
return undefined;
|
|
13840
14331
|
}
|
|
13841
14332
|
const lastSegment = address.pathInfo.segments[address.pathInfo.segments.length - 1];
|
|
14333
|
+
// 公開 getter の dispatch(docs/state-overlay-export-design.md §2-1): 掛かるのは
|
|
14334
|
+
// 「ツリーの未存在キー」の分岐だけ(X1 — 命中する読みは無改造)。マウントの無い
|
|
14335
|
+
// state は boolean 1 個で抜ける(D18)
|
|
14336
|
+
if (stateElement.hasMounts === true && lastSegment !== WILDCARD
|
|
14337
|
+
&& !(lastSegment in Object(parentValue))) {
|
|
14338
|
+
const exported = resolveExport(stateElement, parentAddress.pathInfo.path, lastSegment, address.listIndex);
|
|
14339
|
+
if (exported !== null) {
|
|
14340
|
+
return readExportedAccessor(exported.record, exported.entry, address.listIndex, receiver, handler);
|
|
14341
|
+
}
|
|
14342
|
+
}
|
|
13842
14343
|
if (lastSegment === WILDCARD) {
|
|
13843
14344
|
// listIndex が無いまま末尾ワイルドカードに到達 = そのパスの階数を満たす
|
|
13844
14345
|
// ループ文脈が無い(`matrix.*.*` を 1 段の `for` の中で読む等)。元の文面は
|
|
@@ -14675,6 +15176,34 @@ function notifyWrite(address, absAddress, receiver, handler, keyedMergePath) {
|
|
|
14675
15176
|
// $postUpdate の手動リフレッシュは従来通り全行展開のまま)
|
|
14676
15177
|
{ listExpansion: "diff", keyedMergePath });
|
|
14677
15178
|
}
|
|
15179
|
+
/**
|
|
15180
|
+
* 書き込み完了後のキャッシュ整合(Issue #234)。
|
|
15181
|
+
*
|
|
15182
|
+
* ワイルドカードのデータパス(リスト行)は代入値がそのまま格納値なので、
|
|
15183
|
+
* 代入値を dirty:false で載せて次回の読みを省く。
|
|
15184
|
+
*
|
|
15185
|
+
* アクセサペア(getterPaths に載るパス)は getter が正本であり、setter は
|
|
15186
|
+
* 命令的な代入に過ぎない。代入値を getter の評価結果として固定すると
|
|
15187
|
+
* - setter が正規化・分配した結果と読みが食い違う
|
|
15188
|
+
* - getter が一度も評価されず動的依存が張られない → 依存先を書いても
|
|
15189
|
+
* walkDependency がこのキャッシュを dirty にできず、永続的に stale になる
|
|
15190
|
+
* (プリミティブ代入は同値ガードの旧値読みで偶然 getter が走るが、
|
|
15191
|
+
* オブジェクト代入は同値ガードを素通りするため救済がない)
|
|
15192
|
+
* ため、キャッシュを dirty にして次回の読みで getter を再評価させる。
|
|
15193
|
+
*/
|
|
15194
|
+
function commitWriteCache(stateElement, path, absAddress, value, cacheable) {
|
|
15195
|
+
if (!cacheable) {
|
|
15196
|
+
return;
|
|
15197
|
+
}
|
|
15198
|
+
if (stateElement.getterPaths.has(path)) {
|
|
15199
|
+
dirtyCacheEntryByAbsoluteStateAddress(absAddress);
|
|
15200
|
+
return;
|
|
15201
|
+
}
|
|
15202
|
+
setCacheEntryByAbsoluteStateAddress(absAddress, {
|
|
15203
|
+
value: value,
|
|
15204
|
+
dirty: false
|
|
15205
|
+
});
|
|
15206
|
+
}
|
|
14678
15207
|
function _setByAddress(target, address, absAddress, value, receiver, handler, keyedMergePath) {
|
|
14679
15208
|
try {
|
|
14680
15209
|
if (address.pathInfo.path in target) {
|
|
@@ -14711,6 +15240,8 @@ function _setByAddress(target, address, absAddress, value, receiver, handler, ke
|
|
|
14711
15240
|
return Reflect.set(parentValue, index, value);
|
|
14712
15241
|
}
|
|
14713
15242
|
else {
|
|
15243
|
+
// 公開 getter への書き込み(X9)は setByAddressCore の fast path(親がオブジェクトの
|
|
15244
|
+
// 未存在キー)で dispatch 済み。ここに来るのは親が非オブジェクトの形だけ
|
|
14714
15245
|
return Reflect.set(parentValue, lastSegment, value);
|
|
14715
15246
|
}
|
|
14716
15247
|
}
|
|
@@ -14886,21 +15417,34 @@ function setByAddressCore(target, address, value, receiver, handler, keyedMergeP
|
|
|
14886
15417
|
});
|
|
14887
15418
|
}
|
|
14888
15419
|
recordWatchPrevValue(stateElement, path, absAddress, devOldValue, devHasOldValue);
|
|
15420
|
+
let dispatchedExport = false;
|
|
14889
15421
|
try {
|
|
14890
15422
|
if (key === undefined) {
|
|
14891
15423
|
// fast path 版の同じ取り違え(末尾ワイルドカードに listIndex が無い)。
|
|
14892
15424
|
// 通常経路と同じ語彙で「何段必要か」を言う(pathDiagnostics.ts)。
|
|
14893
15425
|
raiseError(wildcardScopeMessage(`path "${path}"`, address.pathInfo.wildcardCount, address.listIndex?.length ?? 0));
|
|
14894
15426
|
}
|
|
15427
|
+
// 公開 getter への書き込み(docs/state-overlay-export-design.md X9): 未存在キーへの
|
|
15428
|
+
// 書き込みは今日「ツリーに作る」が、その位置に公開 getter があると以後ツリーが勝ち
|
|
15429
|
+
// (X1)getter を無言で隠す。setter があれば setter、無ければ raise(overlay の set)
|
|
15430
|
+
if (stateElement.hasMounts === true && lastSegment !== WILDCARD && !(key in parentValue)) {
|
|
15431
|
+
const exported = resolveExport(stateElement, address.parentAddress.pathInfo.path, lastSegment, address.listIndex);
|
|
15432
|
+
if (exported !== null) {
|
|
15433
|
+
dispatchedExport = true;
|
|
15434
|
+
return writeExportedAccessor(exported.record, exported.entry, address.listIndex, value, receiver, handler);
|
|
15435
|
+
}
|
|
15436
|
+
}
|
|
14895
15437
|
return Reflect.set(parentValue, key, value);
|
|
14896
15438
|
}
|
|
14897
15439
|
finally {
|
|
14898
15440
|
notifyWrite(address, absAddress, receiver, handler, keyedMergePath);
|
|
14899
|
-
if (
|
|
14900
|
-
|
|
14901
|
-
|
|
14902
|
-
|
|
14903
|
-
|
|
15441
|
+
if (dispatchedExport) {
|
|
15442
|
+
// Exported row paths are cacheable but absent from getterPaths. The
|
|
15443
|
+
// accessor may normalize or reject the input; never pin that input.
|
|
15444
|
+
dirtyCacheEntryByAbsoluteStateAddress(absAddress);
|
|
15445
|
+
}
|
|
15446
|
+
else {
|
|
15447
|
+
commitWriteCache(stateElement, path, absAddress, value, cacheable);
|
|
14904
15448
|
}
|
|
14905
15449
|
// DCC bindable イベントディスパッチ(完全一致 + サブパス → 先頭セグメント、§2.1)
|
|
14906
15450
|
dispatchBindableEvent(stateElement, address.pathInfo, { value });
|
|
@@ -14948,12 +15492,7 @@ function setByAddressCore(target, address, value, receiver, handler, keyedMergeP
|
|
|
14948
15492
|
}
|
|
14949
15493
|
}
|
|
14950
15494
|
finally {
|
|
14951
|
-
|
|
14952
|
-
setCacheEntryByAbsoluteStateAddress(absAddress, {
|
|
14953
|
-
value: value,
|
|
14954
|
-
dirty: false
|
|
14955
|
-
});
|
|
14956
|
-
}
|
|
15495
|
+
commitWriteCache(stateElement, path, absAddress, value, cacheable);
|
|
14957
15496
|
// DCC bindable イベントディスパッチ(完全一致 + サブパス → 先頭セグメント、§2.1)
|
|
14958
15497
|
dispatchBindableEvent(stateElement, address.pathInfo, { value });
|
|
14959
15498
|
}
|
|
@@ -15406,6 +15945,27 @@ function updatedCallback(target, refs, receiver, handler) {
|
|
|
15406
15945
|
return result;
|
|
15407
15946
|
}
|
|
15408
15947
|
|
|
15948
|
+
/**
|
|
15949
|
+
* errorCallback.ts
|
|
15950
|
+
*
|
|
15951
|
+
* StateClass のライフサイクルフック「$errorCallback」を呼び出すユーティリティ関数。
|
|
15952
|
+
*
|
|
15953
|
+
* 主な役割:
|
|
15954
|
+
* - target に $errorCallback メソッドが定義されていれば、(error, info) で呼び出す
|
|
15955
|
+
* - this は writable な state proxy(receiver)— 作者はここで自分の state にエラーを書ける
|
|
15956
|
+
*
|
|
15957
|
+
* 設計ポイント:
|
|
15958
|
+
* - Reflect.get で安全に取得し、無ければ何もしない(disconnectedCallback と同型)
|
|
15959
|
+
* - 呼び出し元(apply/applyChangeFromBindings.ts)が drain 末尾でまとめて呼び、
|
|
15960
|
+
* callback 自身の throw もそこで隔離する。ここでは await しない
|
|
15961
|
+
*/
|
|
15962
|
+
function errorCallback(target, error, info, receiver, _handler) {
|
|
15963
|
+
const callback = Reflect.get(target, STATE_ERROR_CALLBACK_NAME);
|
|
15964
|
+
if (typeof callback === "function") {
|
|
15965
|
+
callback.call(receiver, error, info);
|
|
15966
|
+
}
|
|
15967
|
+
}
|
|
15968
|
+
|
|
15409
15969
|
/**
|
|
15410
15970
|
* setLoopContext.ts
|
|
15411
15971
|
*
|
|
@@ -15639,6 +16199,12 @@ function get(target, prop, receiver, handler) {
|
|
|
15639
16199
|
};
|
|
15640
16200
|
break;
|
|
15641
16201
|
}
|
|
16202
|
+
case errorCallbackSymbol: {
|
|
16203
|
+
api = (error, info) => {
|
|
16204
|
+
return errorCallback(target, error, info, receiver);
|
|
16205
|
+
};
|
|
16206
|
+
break;
|
|
16207
|
+
}
|
|
15642
16208
|
default: {
|
|
15643
16209
|
return Reflect.get(target, prop, receiver);
|
|
15644
16210
|
}
|
|
@@ -15957,6 +16523,11 @@ function initializeMountScope(record, scopeRoot) {
|
|
|
15957
16523
|
setStateElementAlias(scopeRoot, record.parentStateElement);
|
|
15958
16524
|
}
|
|
15959
16525
|
buildMountScopeBindings(record, scopeRoot);
|
|
16526
|
+
// Register exports and alias edges once. Notify parents that evaluated before
|
|
16527
|
+
// registration, including on reinitialization when values may have changed.
|
|
16528
|
+
registerExports(record);
|
|
16529
|
+
warnShadowedExports(record);
|
|
16530
|
+
notifyExports(record);
|
|
15960
16531
|
setBindingsReadyForScope(scopeRoot, Promise.resolve());
|
|
15961
16532
|
}
|
|
15962
16533
|
function buildMountScopeBindings(record, walkRoot) {
|
|
@@ -15990,6 +16561,8 @@ function remountScopeBindings(record, scopeRoot) {
|
|
|
15990
16561
|
const rebound = session.rebindAddresses();
|
|
15991
16562
|
// 空でも呼んで良い(ループが回らないだけ)— 分岐を持たない
|
|
15992
16563
|
applyChangeFromBindings(rebound);
|
|
16564
|
+
// 別の行に付け替わった = その行の公開パスの答えが変わった(X6)
|
|
16565
|
+
notifyExports(record);
|
|
15993
16566
|
}
|
|
15994
16567
|
|
|
15995
16568
|
/**
|
|
@@ -16430,6 +17003,8 @@ class State extends HTMLElementBase {
|
|
|
16430
17003
|
}
|
|
16431
17004
|
__state;
|
|
16432
17005
|
_hasUpdatedCallback = false;
|
|
17006
|
+
/** $errorCallback の有無(_hasUpdatedCallback と同じく state セット時に確定。ルートのみ) */
|
|
17007
|
+
_hasErrorCallback = false;
|
|
16433
17008
|
/** enable-ssr のスナップショットから初期化された(D14: ボリュームはデータを採用する) */
|
|
16434
17009
|
_hydratedFromSsr = false;
|
|
16435
17010
|
// 他行を読む getter が検出されたリストパス(diff-filter 展開の全行フォールバック対象)。
|
|
@@ -16519,6 +17094,7 @@ class State extends HTMLElementBase {
|
|
|
16519
17094
|
// パターンは検知できない(bindProperty / _state 再セットは検知する)。
|
|
16520
17095
|
// ライフサイクルフックは宣言時に定義するのが規約。
|
|
16521
17096
|
this._hasUpdatedCallback = STATE_UPDATED_CALLBACK_NAME in value;
|
|
17097
|
+
this._hasErrorCallback = STATE_ERROR_CALLBACK_NAME in value;
|
|
16522
17098
|
// 再 set 時に二重 subscribe しないよう registry をクリアしてから $on を配線し直す。
|
|
16523
17099
|
clearEventTokenRegistry(this);
|
|
16524
17100
|
processOnDeclaration(this, value, this._eventTokenNames);
|
|
@@ -17118,6 +17694,9 @@ class State extends HTMLElementBase {
|
|
|
17118
17694
|
// 台帳エイリアスは消さない(プール再利用の再接続が同じスコープに戻る)。
|
|
17119
17695
|
// $disconnectedCallback だけは要素のライフサイクルとして呼ぶ(例外は隔離)
|
|
17120
17696
|
callMountLifecycleCallback(this._mountRecord, "$disconnectedCallback");
|
|
17697
|
+
// 公開 getter の答えが消えた(X6)— 親の依存者を再評価させる。プール返却も
|
|
17698
|
+
// 恒久破棄もここを通る(行ごと消えた形は $postUpdate が届かず無視される)
|
|
17699
|
+
notifyExports(this._mountRecord);
|
|
17121
17700
|
this._rootNode = null;
|
|
17122
17701
|
return;
|
|
17123
17702
|
}
|
|
@@ -17366,6 +17945,9 @@ class State extends HTMLElementBase {
|
|
|
17366
17945
|
get hasUpdatedCallback() {
|
|
17367
17946
|
return this._hasUpdatedCallback;
|
|
17368
17947
|
}
|
|
17948
|
+
get hasErrorCallback() {
|
|
17949
|
+
return this._hasErrorCallback;
|
|
17950
|
+
}
|
|
17369
17951
|
get crossRowListPaths() {
|
|
17370
17952
|
return this._crossRowListPaths;
|
|
17371
17953
|
}
|
|
@@ -17725,6 +18307,7 @@ function getWcsManifest() {
|
|
|
17725
18307
|
STATE_CONNECTED_CALLBACK_NAME,
|
|
17726
18308
|
STATE_DISCONNECTED_CALLBACK_NAME,
|
|
17727
18309
|
STATE_UPDATED_CALLBACK_NAME,
|
|
18310
|
+
STATE_ERROR_CALLBACK_NAME,
|
|
17728
18311
|
WEBCOMPONENT_STATE_READY_CALLBACK_NAME,
|
|
17729
18312
|
],
|
|
17730
18313
|
reservedStateApi: [
|
|
@@ -17871,5 +18454,5 @@ function resolveLiveDeclaration(tag) {
|
|
|
17871
18454
|
return { propertyEvents, inputs, commands };
|
|
17872
18455
|
}
|
|
17873
18456
|
|
|
17874
|
-
export { Ssr, VERSION, WCS_MANIFEST_VERSION, analyzeContract, bootstrapState, buildBindings, builtinFilterMeta, defineState, getBindingsReady, getConfig, getWcsManifest };
|
|
18457
|
+
export { Ssr, TRUSTED_TYPES_POLICY_SLOT, VERSION, WCS_MANIFEST_VERSION, analyzeContract, bootstrapState, buildBindings, builtinFilterMeta, defineState, getBindingsReady, getConfig, getTrustedTypesPolicy, getWcsManifest, setTrustedTypesPolicy };
|
|
17875
18458
|
//# sourceMappingURL=index.esm.js.map
|