@wcstack/state 1.26.0 → 1.28.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 +100 -8
- package/README.md +103 -7
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.d.ts +70 -3
- package/dist/index.esm.js +1407 -174
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.d.ts +35 -0
- package/dist/manifest.esm.js +213 -2
- package/dist/parser.d.ts +74 -0
- package/dist/parser.esm.js +1452 -0
- package/dist/wcs-manifest.json +112 -1
- package/package.json +5 -1
package/dist/index.esm.js
CHANGED
|
@@ -82,47 +82,6 @@ function setConfig(partialConfig) {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
const bindingPromiseByNode = new WeakMap();
|
|
86
|
-
// resolve 済みマーク。エントリ未生成のまま resolve されたノードは、後から
|
|
87
|
-
// wait された時に「生成して即 resolve」で追いつく。
|
|
88
|
-
const resolvedNodes = new WeakSet();
|
|
89
|
-
let id$1 = 0;
|
|
90
|
-
function getInitializeBindingPromiseByNode(node) {
|
|
91
|
-
let bindingPromise = bindingPromiseByNode.get(node) || null;
|
|
92
|
-
if (bindingPromise !== null) {
|
|
93
|
-
return bindingPromise;
|
|
94
|
-
}
|
|
95
|
-
let resolveFn = undefined;
|
|
96
|
-
const promise = new Promise((resolve) => {
|
|
97
|
-
resolveFn = resolve;
|
|
98
|
-
});
|
|
99
|
-
bindingPromise = {
|
|
100
|
-
id: ++id$1,
|
|
101
|
-
promise,
|
|
102
|
-
resolve: resolveFn
|
|
103
|
-
};
|
|
104
|
-
bindingPromiseByNode.set(node, bindingPromise);
|
|
105
|
-
if (resolvedNodes.has(node)) {
|
|
106
|
-
bindingPromise.resolve();
|
|
107
|
-
}
|
|
108
|
-
return bindingPromise;
|
|
109
|
-
}
|
|
110
|
-
async function waitInitializeBinding(node) {
|
|
111
|
-
const bindingPromise = getInitializeBindingPromiseByNode(node);
|
|
112
|
-
await bindingPromise.promise;
|
|
113
|
-
}
|
|
114
|
-
function resolveInitializedBinding(node) {
|
|
115
|
-
// ホットパス: リスト行では全 subscriber ノードがここを通るが、await する消費者
|
|
116
|
-
// (boundComponent / shadowRoot host)はほぼ居ない。既存エントリが無ければ
|
|
117
|
-
// Promise+closure を生成せず resolve 済みマークだけ残す(15 万個級の割り当て削減)。
|
|
118
|
-
const existing = bindingPromiseByNode.get(node);
|
|
119
|
-
if (typeof existing !== "undefined") {
|
|
120
|
-
existing.resolve();
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
resolvedNodes.add(node);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
85
|
const DELIMITER = '.';
|
|
127
86
|
const WILDCARD = '*';
|
|
128
87
|
const MAX_WILDCARD_DEPTH = 128;
|
|
@@ -130,6 +89,23 @@ const MAX_LOOP_DEPTH = 128;
|
|
|
130
89
|
// 因果伝播(Phase 3)の 1 transaction あたり hop 上限。超過分の未処理 record は
|
|
131
90
|
// quarantine し(適用済みの値は戻さない)、updater から例外は投げない。
|
|
132
91
|
const MAX_PROPAGATION_HOPS = 32;
|
|
92
|
+
// `$watch` ハンドラ起点の書き込み連鎖の打ち切り深さ(docs/state-watch-hook-design.md §7-2)。
|
|
93
|
+
// watch ハンドラ内の書き込みは新しい microtask バッチを作るため MAX_PROPAGATION_HOPS の
|
|
94
|
+
// ガードが効かず、書き込み先が動的なので `$streams` のような静的な自己依存検出もできない。
|
|
95
|
+
// 値は MAX_PROPAGATION_HOPS と同値だが、別の打ち切り機構なので定数は共有しない。
|
|
96
|
+
const MAX_WATCH_CHAIN_DEPTH = 32;
|
|
97
|
+
// updater の drain 終了リスナーの実行順(昇順に呼ばれる。設計書 §3-2 層 1)。
|
|
98
|
+
// watch が先なのは、watch ハンドラの書き込みが同じバッチの stream restart 判定に
|
|
99
|
+
// 影響しないようにするため(watch → restart の一方向)。import 順に順序を持たせると
|
|
100
|
+
// 無関係な import 整理で静かに壊れるため、明示的な優先度で固定する。
|
|
101
|
+
//
|
|
102
|
+
// devtools が最も先なのは、`state:update-batch` が「そのバッチに何が載ったか」の
|
|
103
|
+
// 観測であり、watch / restart の副作用が乗る前の生の集合を報告すべきだから。
|
|
104
|
+
// 既定値 0 のまま暗黙に先頭へ入るのに任せず、意図として定数で固定する
|
|
105
|
+
// (docs/devtools-hook-protocol.md §4.3)。
|
|
106
|
+
const DEVTOOLS_LISTENER_PRIORITY = 0;
|
|
107
|
+
const WATCH_LISTENER_PRIORITY = 10;
|
|
108
|
+
const STREAM_LISTENER_PRIORITY = 20;
|
|
133
109
|
// data-wcs バインディング構文 `[prop][#mod]: [path][@state][|filter...]` の区切り文字(単一正本)。
|
|
134
110
|
// これらは「死守の壁(構文契約)」であり値は不変。manifest.syntax.delimiters で公開される。
|
|
135
111
|
const BINDING_SEPARATOR = ';'; // 複数バインディングの区切り
|
|
@@ -137,6 +113,38 @@ const PROP_VALUE_SEPARATOR = ':'; // 左辺(prop)と右辺(path)の区切り
|
|
|
137
113
|
const MODIFIER_SEPARATOR = '#'; // prop と修飾子の区切り
|
|
138
114
|
const STATE_NAME_SEPARATOR = '@'; // path と @stateName の区切り
|
|
139
115
|
const FILTER_SEPARATOR = '|'; // フィルタパイプの区切り
|
|
116
|
+
// 修飾子(`#` 後)の語彙(単一正本)。manifest.syntax.modifiers で公開される。
|
|
117
|
+
// フラグ形(`#prevent` — 値を取らない)とキー値形(`#init=element` — `=` で値を取る)。
|
|
118
|
+
// 消費箇所(event/handler・BindingSession・twowayHandler・bindings/initialSync)は
|
|
119
|
+
// この定数を参照する — 文字列リテラルの散在は tooling への収載漏れの温床だった
|
|
120
|
+
// (docs/static-wiring-dx-design.md §2-2)。
|
|
121
|
+
const MODIFIER_PREVENT = 'prevent';
|
|
122
|
+
const MODIFIER_STOP = 'stop';
|
|
123
|
+
const MODIFIER_READONLY = 'ro';
|
|
124
|
+
const MODIFIER_FLAGS = Object.freeze([
|
|
125
|
+
MODIFIER_PREVENT, MODIFIER_STOP, MODIFIER_READONLY,
|
|
126
|
+
]);
|
|
127
|
+
const MODIFIER_KEY_INIT = 'init';
|
|
128
|
+
const MODIFIER_KEY_SYNC = 'sync';
|
|
129
|
+
const MODIFIER_KEYS = Object.freeze([
|
|
130
|
+
MODIFIER_KEY_INIT, MODIFIER_KEY_SYNC,
|
|
131
|
+
]);
|
|
132
|
+
// bindingType 判別と左辺 namespace の語彙(単一正本)。manifest.syntax.bindingTypes で
|
|
133
|
+
// 公開される。パーサ(parseBindTextsForElement)とイベント層はこの定数に分岐する。
|
|
134
|
+
// apply 層のディスパッチマップ(apply/applyChange.ts の applyChangeByFirstSegment)の
|
|
135
|
+
// キー集合との一致は __tests__/manifest.test.ts の drift テストが強制する —
|
|
136
|
+
// manifest エントリ(DOM 非依存)から apply 層を import しないための分離。
|
|
137
|
+
const ELSE_KEYWORD = 'else';
|
|
138
|
+
const SPREAD_PROP = '...';
|
|
139
|
+
const EVENT_PROP_PREFIX = 'on';
|
|
140
|
+
const EVENT_TOKEN_NAMESPACE = 'eventToken';
|
|
141
|
+
const COMMAND_NAMESPACE = 'command';
|
|
142
|
+
const CLASS_NAMESPACE = 'class';
|
|
143
|
+
const ATTR_NAMESPACE = 'attr';
|
|
144
|
+
const STYLE_NAMESPACE = 'style';
|
|
145
|
+
// リストインデックス参照名(`$1`..`$N`)の接頭辞(単一正本)。
|
|
146
|
+
// manifest.syntax.indexParam で公開される。
|
|
147
|
+
const INDEX_PARAM_PREFIX = '$';
|
|
140
148
|
/**
|
|
141
149
|
* stackIndexByIndexName
|
|
142
150
|
* インデックス名からスタックインデックスへのマッピング
|
|
@@ -148,7 +156,7 @@ const FILTER_SEPARATOR = '|'; // フィルタパイプの区切り
|
|
|
148
156
|
*/
|
|
149
157
|
const tmpIndexByIndexName = {};
|
|
150
158
|
for (let i = 0; i < MAX_WILDCARD_DEPTH; i++) {
|
|
151
|
-
tmpIndexByIndexName[
|
|
159
|
+
tmpIndexByIndexName[`${INDEX_PARAM_PREFIX}${i + 1}`] = i;
|
|
152
160
|
}
|
|
153
161
|
const INDEX_BY_INDEX_NAME = Object.freeze(tmpIndexByIndexName);
|
|
154
162
|
const NO_SET_TIMEOUT = 60 * 1000; // 1分
|
|
@@ -164,11 +172,53 @@ const STATE_COMMAND_NAMESPACE_NAME = "$command";
|
|
|
164
172
|
const STATE_EVENT_TOKENS_NAME = "$eventTokens";
|
|
165
173
|
const STATE_ON_NAME = "$on";
|
|
166
174
|
const STATE_STREAMS_NAME = "$streams";
|
|
175
|
+
const STATE_WATCH_NAME = "$watch";
|
|
167
176
|
const STATE_LIST_KEYS_NAME = "$listKeys";
|
|
168
177
|
const STATE_STREAM_STATUS_NAMESPACE_NAME = "$streamStatus";
|
|
169
178
|
const STATE_STREAM_ERROR_NAMESPACE_NAME = "$streamError";
|
|
170
179
|
const DCC_DEFINITION_ATTRIBUTE = "data-wc-definition";
|
|
171
180
|
|
|
181
|
+
const bindingPromiseByNode = new WeakMap();
|
|
182
|
+
// resolve 済みマーク。エントリ未生成のまま resolve されたノードは、後から
|
|
183
|
+
// wait された時に「生成して即 resolve」で追いつく。
|
|
184
|
+
const resolvedNodes = new WeakSet();
|
|
185
|
+
let id$1 = 0;
|
|
186
|
+
function getInitializeBindingPromiseByNode(node) {
|
|
187
|
+
let bindingPromise = bindingPromiseByNode.get(node) || null;
|
|
188
|
+
if (bindingPromise !== null) {
|
|
189
|
+
return bindingPromise;
|
|
190
|
+
}
|
|
191
|
+
let resolveFn = undefined;
|
|
192
|
+
const promise = new Promise((resolve) => {
|
|
193
|
+
resolveFn = resolve;
|
|
194
|
+
});
|
|
195
|
+
bindingPromise = {
|
|
196
|
+
id: ++id$1,
|
|
197
|
+
promise,
|
|
198
|
+
resolve: resolveFn
|
|
199
|
+
};
|
|
200
|
+
bindingPromiseByNode.set(node, bindingPromise);
|
|
201
|
+
if (resolvedNodes.has(node)) {
|
|
202
|
+
bindingPromise.resolve();
|
|
203
|
+
}
|
|
204
|
+
return bindingPromise;
|
|
205
|
+
}
|
|
206
|
+
async function waitInitializeBinding(node) {
|
|
207
|
+
const bindingPromise = getInitializeBindingPromiseByNode(node);
|
|
208
|
+
await bindingPromise.promise;
|
|
209
|
+
}
|
|
210
|
+
function resolveInitializedBinding(node) {
|
|
211
|
+
// ホットパス: リスト行では全 subscriber ノードがここを通るが、await する消費者
|
|
212
|
+
// (boundComponent / shadowRoot host)はほぼ居ない。既存エントリが無ければ
|
|
213
|
+
// Promise+closure を生成せず resolve 済みマークだけ残す(15 万個級の割り当て削減)。
|
|
214
|
+
const existing = bindingPromiseByNode.get(node);
|
|
215
|
+
if (typeof existing !== "undefined") {
|
|
216
|
+
existing.resolve();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
resolvedNodes.add(node);
|
|
220
|
+
}
|
|
221
|
+
|
|
172
222
|
const _cache$4 = new Map();
|
|
173
223
|
let id = 0;
|
|
174
224
|
function getPathInfo(path) {
|
|
@@ -622,6 +672,75 @@ const STRUCTURAL_BINDING_TYPE_SET = new Set([
|
|
|
622
672
|
"for",
|
|
623
673
|
]);
|
|
624
674
|
|
|
675
|
+
/**
|
|
676
|
+
* errorGuidance.ts — エラーメッセージへの self-fix 誘導(GTM 2-5 /
|
|
677
|
+
* docs/static-wiring-dx-design.md §3)。
|
|
678
|
+
*
|
|
679
|
+
* コンソールは「書き手(人間・AI とも)が誤った瞬間に必ず読む面」なので、
|
|
680
|
+
* (a) did-you-mean 候補 (b) lint への誘導 をエラーメッセージ自体に埋め込む。
|
|
681
|
+
* ここの関数は全て**エラーパスでのみ**呼ばれる — 正常系のコストはゼロ。
|
|
682
|
+
* auto.min.js に同梱されるため文字列は最小限に保つ(エラーパス専用モジュールの
|
|
683
|
+
* 遅延 import は `src/auto.ts` の SRI 自己完結制約で不可)。
|
|
684
|
+
*
|
|
685
|
+
* 診断 code の語彙はコンソール → lint → IDE の三面で共有する:
|
|
686
|
+
* メッセージ先頭の `[wcs/...]` は wcstack-intellisense / @wcstack/lint の
|
|
687
|
+
* 安定診断 code(packages/vscode-wcs/src/core/diagnostics.ts)と同一。
|
|
688
|
+
*/
|
|
689
|
+
/** 挿入・削除・置換の編集距離。長さ差が max を超えたら早期に max+1 を返す。 */
|
|
690
|
+
function editDistance(a, b, max) {
|
|
691
|
+
if (Math.abs(a.length - b.length) > max) {
|
|
692
|
+
return max + 1;
|
|
693
|
+
}
|
|
694
|
+
const prev = new Array(b.length + 1);
|
|
695
|
+
const curr = new Array(b.length + 1);
|
|
696
|
+
for (let j = 0; j <= b.length; j++) {
|
|
697
|
+
prev[j] = j;
|
|
698
|
+
}
|
|
699
|
+
for (let i = 1; i <= a.length; i++) {
|
|
700
|
+
curr[0] = i;
|
|
701
|
+
for (let j = 1; j <= b.length; j++) {
|
|
702
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
703
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
704
|
+
}
|
|
705
|
+
for (let j = 0; j <= b.length; j++) {
|
|
706
|
+
prev[j] = curr[j];
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return prev[b.length];
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* 候補集合から編集距離 2 以内の最近傍を探し、` Did you mean "<best>"?` を返す。
|
|
713
|
+
* 該当なしは空文字。規準(距離 2・同距離は先勝ち・大小文字は畳んで比較)は
|
|
714
|
+
* lint の did-you-mean(ioNodeValidator の suggestion)と同じ — 三面で提案が
|
|
715
|
+
* 割れないように揃えている。動的キー等で候補が列挙できないサイトでは呼ばない
|
|
716
|
+
* = 誘導文のみに縮退(設計 §3 の縮退)。
|
|
717
|
+
*/
|
|
718
|
+
function didYouMean(input, candidates) {
|
|
719
|
+
// 空入力(`a|` の末尾パイプ等)に短い候補を提案しても無意味なので出さない。
|
|
720
|
+
if (input.length === 0) {
|
|
721
|
+
return "";
|
|
722
|
+
}
|
|
723
|
+
const folded = input.toLowerCase();
|
|
724
|
+
let best = null;
|
|
725
|
+
let bestDistance = 3;
|
|
726
|
+
for (const candidate of candidates) {
|
|
727
|
+
const distance = editDistance(folded, candidate.toLowerCase(), 2);
|
|
728
|
+
if (distance < bestDistance) {
|
|
729
|
+
best = candidate;
|
|
730
|
+
bestDistance = distance;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return best !== null ? ` Did you mean "${best}"?` : "";
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* lint への誘導(誘導付きメッセージ共通の一文)。
|
|
737
|
+
* **lint が実際にそのケースを検出するサイトにだけ付ける** — 検出しないケースに
|
|
738
|
+
* 付けると「エラー → lint 実行 → clean」の空振りで検証ループの信頼を毀損する
|
|
739
|
+
* (DCC 宣言・watch の一部 shape・構造型単独バインディング違反は lint 未検出のため
|
|
740
|
+
* 付けない。lint 側への検査追加は follow-up)。
|
|
741
|
+
*/
|
|
742
|
+
const LINT_HINT = " Validate statically: npx @wcstack/lint <file>.";
|
|
743
|
+
|
|
625
744
|
/**
|
|
626
745
|
* errorMessages.ts
|
|
627
746
|
*
|
|
@@ -683,6 +802,15 @@ function valueMustBeBoolean(fnName) {
|
|
|
683
802
|
function valueMustBeDate(fnName) {
|
|
684
803
|
raiseError(`filter ${fnName} requires a date value`);
|
|
685
804
|
}
|
|
805
|
+
/**
|
|
806
|
+
* Throws error when filter requires array value but non-array provided.
|
|
807
|
+
*
|
|
808
|
+
* @param fnName - Name of the filter function
|
|
809
|
+
* @returns Never returns (always throws)
|
|
810
|
+
*/
|
|
811
|
+
function valueMustBeArray(fnName) {
|
|
812
|
+
raiseError(`filter ${fnName} requires an array value`);
|
|
813
|
+
}
|
|
686
814
|
|
|
687
815
|
/**
|
|
688
816
|
* builtinFilters.ts
|
|
@@ -695,7 +823,7 @@ function valueMustBeDate(fnName) {
|
|
|
695
823
|
* - Designed for common use as both input and output filters
|
|
696
824
|
*
|
|
697
825
|
* Design points:
|
|
698
|
-
* - Comprehensive coverage of diverse filters: eq, ne, lt, gt, inc, fix, locale, uc, lc, cap, trim, slice, pad, int, float, round, date, time, ymd, falsy, truthy, defaults, boolean, number, string, null, etc.
|
|
826
|
+
* - Comprehensive coverage of diverse filters: eq, ne, lt, gt, inc, abs, clamp, fix, locale, uc, lc, cap, trim, slice, pad, truncate, join, int, float, round, percent, unit, date, time, ymd, hms, falsy, truthy, defaults, boolean, number, string, null, etc.
|
|
699
827
|
* - Rich type checking and error handling for option values
|
|
700
828
|
* - Centralized management of filter functions with FilterWithOptions type, easy to extend
|
|
701
829
|
* - Dynamic retrieval of filter functions from filter names and options via builtinFilterFn
|
|
@@ -928,6 +1056,48 @@ const mod = (options) => {
|
|
|
928
1056
|
return value % Number(opt);
|
|
929
1057
|
};
|
|
930
1058
|
};
|
|
1059
|
+
/**
|
|
1060
|
+
* Absolute value filter - returns the magnitude of a number.
|
|
1061
|
+
*
|
|
1062
|
+
* @param options - Unused
|
|
1063
|
+
* @returns Filter function that returns the absolute value
|
|
1064
|
+
*/
|
|
1065
|
+
const abs = (_options) => {
|
|
1066
|
+
return (value) => {
|
|
1067
|
+
if (typeof value !== 'number') {
|
|
1068
|
+
valueMustBeNumber('abs');
|
|
1069
|
+
}
|
|
1070
|
+
return Math.abs(value);
|
|
1071
|
+
};
|
|
1072
|
+
};
|
|
1073
|
+
/**
|
|
1074
|
+
* Clamp filter - constrains a number to the inclusive range [min, max].
|
|
1075
|
+
*
|
|
1076
|
+
* Saturating conversion in the same family as round/floor/ceil, so it stays on
|
|
1077
|
+
* the wire rather than in state. Pairs with `unit` for style bindings:
|
|
1078
|
+
* `style.width: ratio|clamp(0,1)|percent(0)`.
|
|
1079
|
+
*
|
|
1080
|
+
* @param options - Array with minimum as first element and maximum as second (both required)
|
|
1081
|
+
* @returns Filter function that returns the clamped number
|
|
1082
|
+
*/
|
|
1083
|
+
const clamp = (options) => {
|
|
1084
|
+
const opt1 = options?.[0] ?? optionsRequired('clamp');
|
|
1085
|
+
if (!validateNumberString(opt1)) {
|
|
1086
|
+
optionMustBeNumber('clamp');
|
|
1087
|
+
}
|
|
1088
|
+
const opt2 = options?.[1] ?? optionsRequired('clamp');
|
|
1089
|
+
if (!validateNumberString(opt2)) {
|
|
1090
|
+
optionMustBeNumber('clamp');
|
|
1091
|
+
}
|
|
1092
|
+
const min = Number(opt1);
|
|
1093
|
+
const max = Number(opt2);
|
|
1094
|
+
return (value) => {
|
|
1095
|
+
if (typeof value !== 'number') {
|
|
1096
|
+
valueMustBeNumber('clamp');
|
|
1097
|
+
}
|
|
1098
|
+
return Math.min(Math.max(value, min), max);
|
|
1099
|
+
};
|
|
1100
|
+
};
|
|
931
1101
|
/**
|
|
932
1102
|
* Fixed decimal filter - formats number to fixed decimal places.
|
|
933
1103
|
*
|
|
@@ -1194,6 +1364,76 @@ const percent = (options) => {
|
|
|
1194
1364
|
return `${(value * 100).toFixed(Number(opt))}%`;
|
|
1195
1365
|
};
|
|
1196
1366
|
};
|
|
1367
|
+
/**
|
|
1368
|
+
* Unit filter - appends a CSS unit (or any suffix) to the value.
|
|
1369
|
+
*
|
|
1370
|
+
* A number alone does nothing in CSS, so without this the unit has to be built in
|
|
1371
|
+
* state — which drags presentation into the source of truth, and in the worst case
|
|
1372
|
+
* forces a whole derived array just to carry `"42%"` strings.
|
|
1373
|
+
* `style.height: samples.*.cpu|clamp(0,100)|fix(0)|unit(%)` keeps it on the wire.
|
|
1374
|
+
*
|
|
1375
|
+
* Accepts strings as well as numbers **on purpose**: the useful chains run through
|
|
1376
|
+
* `fix` / `percent`, which already return strings. Rejecting non-numbers here would
|
|
1377
|
+
* break exactly the combination this filter exists for.
|
|
1378
|
+
*
|
|
1379
|
+
* `null` / `undefined` pass through untouched rather than becoming `"undefinedpx"`,
|
|
1380
|
+
* so the binding layer's "undefined skips the write, null clears" semantics survive.
|
|
1381
|
+
*
|
|
1382
|
+
* @param options - Array with the unit/suffix as first element (required)
|
|
1383
|
+
* @returns Filter function that returns the value with the unit appended
|
|
1384
|
+
*/
|
|
1385
|
+
const unit = (options) => {
|
|
1386
|
+
const opt = options?.[0] ?? optionsRequired('unit');
|
|
1387
|
+
return (value) => {
|
|
1388
|
+
if (value === null || typeof value === 'undefined') {
|
|
1389
|
+
return value;
|
|
1390
|
+
}
|
|
1391
|
+
return String(value) + opt;
|
|
1392
|
+
};
|
|
1393
|
+
};
|
|
1394
|
+
/**
|
|
1395
|
+
* Join filter - joins array elements into a string.
|
|
1396
|
+
*
|
|
1397
|
+
* The default separator is `", "` rather than `","`: a bare comma is what `String()`
|
|
1398
|
+
* already produces without any filter, so defaulting to it would make `|join` a no-op.
|
|
1399
|
+
*
|
|
1400
|
+
* @param options - Array with separator as first element (default: ', ')
|
|
1401
|
+
* @returns Filter function that returns the joined string
|
|
1402
|
+
*/
|
|
1403
|
+
const join = (options) => {
|
|
1404
|
+
const opt = options?.[0] ?? ', ';
|
|
1405
|
+
return (value) => {
|
|
1406
|
+
if (!Array.isArray(value)) {
|
|
1407
|
+
valueMustBeArray('join');
|
|
1408
|
+
}
|
|
1409
|
+
return value.join(opt);
|
|
1410
|
+
};
|
|
1411
|
+
};
|
|
1412
|
+
/**
|
|
1413
|
+
* Truncate filter - shortens a string and appends an ellipsis.
|
|
1414
|
+
*
|
|
1415
|
+
* The length option counts **kept characters**, not the total including the suffix,
|
|
1416
|
+
* matching the existing `slice(0, n)` reading. A string at or below the limit is
|
|
1417
|
+
* returned untouched (no suffix).
|
|
1418
|
+
*
|
|
1419
|
+
* @param options - Array with max kept length as first element and suffix as second (default: '…')
|
|
1420
|
+
* @returns Filter function that returns the truncated string
|
|
1421
|
+
*/
|
|
1422
|
+
const truncate = (options) => {
|
|
1423
|
+
const opt1 = options?.[0] ?? optionsRequired('truncate');
|
|
1424
|
+
if (!validateNumberString(opt1)) {
|
|
1425
|
+
optionMustBeNumber('truncate');
|
|
1426
|
+
}
|
|
1427
|
+
const maxLength = Number(opt1);
|
|
1428
|
+
const suffix = options?.[1] ?? '…';
|
|
1429
|
+
return (value) => {
|
|
1430
|
+
const v = String(value);
|
|
1431
|
+
if (v.length <= maxLength) {
|
|
1432
|
+
return v;
|
|
1433
|
+
}
|
|
1434
|
+
return v.slice(0, maxLength) + suffix;
|
|
1435
|
+
};
|
|
1436
|
+
};
|
|
1197
1437
|
/**
|
|
1198
1438
|
* Date filter - formats Date object as localized date string.
|
|
1199
1439
|
*
|
|
@@ -1257,6 +1497,27 @@ const ymd = (options) => {
|
|
|
1257
1497
|
return `${year}${opt}${month}${opt}${day}`;
|
|
1258
1498
|
};
|
|
1259
1499
|
};
|
|
1500
|
+
/**
|
|
1501
|
+
* Hour-Minute-Second filter - formats Date object as HH:MM:SS string.
|
|
1502
|
+
*
|
|
1503
|
+
* The counterpart of `ymd`: a fixed, zero-padded, locale-independent rendering with a
|
|
1504
|
+
* configurable separator, for when `time` (locale-formatted) is not stable enough.
|
|
1505
|
+
*
|
|
1506
|
+
* @param options - Array with separator string as first element (default: ':')
|
|
1507
|
+
* @returns Filter function that returns formatted time string
|
|
1508
|
+
*/
|
|
1509
|
+
const hms = (options) => {
|
|
1510
|
+
const opt = options?.[0] ?? ':';
|
|
1511
|
+
return (value) => {
|
|
1512
|
+
if (!(value instanceof Date)) {
|
|
1513
|
+
valueMustBeDate('hms');
|
|
1514
|
+
}
|
|
1515
|
+
const hours = value.getHours().toString().padStart(2, '0');
|
|
1516
|
+
const minutes = value.getMinutes().toString().padStart(2, '0');
|
|
1517
|
+
const seconds = value.getSeconds().toString().padStart(2, '0');
|
|
1518
|
+
return `${hours}${opt}${minutes}${opt}${seconds}`;
|
|
1519
|
+
};
|
|
1520
|
+
};
|
|
1260
1521
|
/**
|
|
1261
1522
|
* Falsy filter - checks if value is falsy.
|
|
1262
1523
|
*
|
|
@@ -1347,6 +1608,8 @@ const builtinFilters = {
|
|
|
1347
1608
|
"mul": mul,
|
|
1348
1609
|
"div": div,
|
|
1349
1610
|
"mod": mod,
|
|
1611
|
+
"abs": abs,
|
|
1612
|
+
"clamp": clamp,
|
|
1350
1613
|
"fix": fix,
|
|
1351
1614
|
"locale": locale,
|
|
1352
1615
|
"uc": uc,
|
|
@@ -1358,16 +1621,20 @@ const builtinFilters = {
|
|
|
1358
1621
|
"pad": pad,
|
|
1359
1622
|
"rep": rep,
|
|
1360
1623
|
"rev": rev,
|
|
1624
|
+
"truncate": truncate,
|
|
1625
|
+
"join": join,
|
|
1361
1626
|
"int": int,
|
|
1362
1627
|
"float": float,
|
|
1363
1628
|
"round": round,
|
|
1364
1629
|
"floor": floor,
|
|
1365
1630
|
"ceil": ceil,
|
|
1366
1631
|
"percent": percent,
|
|
1632
|
+
"unit": unit,
|
|
1367
1633
|
"date": date,
|
|
1368
1634
|
"time": time,
|
|
1369
1635
|
"datetime": datetime,
|
|
1370
1636
|
"ymd": ymd,
|
|
1637
|
+
"hms": hms,
|
|
1371
1638
|
"falsy": falsy,
|
|
1372
1639
|
"truthy": truthy,
|
|
1373
1640
|
"defaults": defaults,
|
|
@@ -1392,16 +1659,50 @@ const builtinFiltersByFilterIOType = {
|
|
|
1392
1659
|
const builtinFilterFn = (name, options) => (filters) => {
|
|
1393
1660
|
const filter = filters[name];
|
|
1394
1661
|
if (!filter) {
|
|
1395
|
-
|
|
1662
|
+
// lint の wcs/filter-unknown と同じ語彙・同じ did-you-mean 規準(三面同語彙)。
|
|
1663
|
+
raiseError(`[wcs/filter-unknown] filter not found: ${name}.${didYouMean(name, Object.keys(filters))}${LINT_HINT}`);
|
|
1396
1664
|
}
|
|
1397
1665
|
return filter(options);
|
|
1398
1666
|
};
|
|
1399
1667
|
|
|
1668
|
+
/**
|
|
1669
|
+
* フィルタ引数リストのパース。`filter(a, b)` の `a, b` 部分を受け取る。
|
|
1670
|
+
*
|
|
1671
|
+
* トリムの規則は「**クォートの外側だけ**」。`fix( 2 )` のような書き癖を吸収するために
|
|
1672
|
+
* 素の引数は前後をトリムするが、クォートは「ここは literal」という宣言なので中身の
|
|
1673
|
+
* 空白は残す。両方まとめてトリムしていたため `pad(5, ' ')` が空文字パディング
|
|
1674
|
+
* (=無変化)に化けており、空白区切りの `join(' / ')` も指定できなかった。
|
|
1675
|
+
*/
|
|
1676
|
+
/** 引数 1 つを確定する。クォート由来の文字が入った範囲より外側だけをトリムする。 */
|
|
1677
|
+
function finalizeArg(text, firstQuoteStart, lastQuoteEnd) {
|
|
1678
|
+
// 先頭側: 最初のクォート文字より前だけが削れる(クォートが無ければ全体が対象)
|
|
1679
|
+
const startLimit = firstQuoteStart === -1 ? text.length : firstQuoteStart;
|
|
1680
|
+
let start = 0;
|
|
1681
|
+
while (start < startLimit && /\s/.test(text[start])) {
|
|
1682
|
+
start++;
|
|
1683
|
+
}
|
|
1684
|
+
// 末尾側: 最後のクォート文字より後ろだけが削れる(クォートが無ければ全体が対象)
|
|
1685
|
+
const endLimit = lastQuoteEnd === -1 ? 0 : lastQuoteEnd;
|
|
1686
|
+
let end = text.length;
|
|
1687
|
+
while (end > endLimit && /\s/.test(text[end - 1])) {
|
|
1688
|
+
end--;
|
|
1689
|
+
}
|
|
1690
|
+
return text.slice(start, end);
|
|
1691
|
+
}
|
|
1400
1692
|
function parseFilterArgs(argsText) {
|
|
1401
1693
|
const args = [];
|
|
1402
1694
|
let current = '';
|
|
1403
1695
|
let inQuote = null;
|
|
1404
1696
|
let hasQuote = false;
|
|
1697
|
+
let firstQuoteStart = -1;
|
|
1698
|
+
let lastQuoteEnd = -1;
|
|
1699
|
+
const flush = () => {
|
|
1700
|
+
args.push(finalizeArg(current, firstQuoteStart, lastQuoteEnd));
|
|
1701
|
+
current = '';
|
|
1702
|
+
hasQuote = false;
|
|
1703
|
+
firstQuoteStart = -1;
|
|
1704
|
+
lastQuoteEnd = -1;
|
|
1705
|
+
};
|
|
1405
1706
|
for (let i = 0; i < argsText.length; i++) {
|
|
1406
1707
|
const char = argsText[i];
|
|
1407
1708
|
if (inQuote) {
|
|
@@ -1409,7 +1710,11 @@ function parseFilterArgs(argsText) {
|
|
|
1409
1710
|
inQuote = null;
|
|
1410
1711
|
}
|
|
1411
1712
|
else {
|
|
1713
|
+
if (firstQuoteStart === -1) {
|
|
1714
|
+
firstQuoteStart = current.length;
|
|
1715
|
+
}
|
|
1412
1716
|
current += char;
|
|
1717
|
+
lastQuoteEnd = current.length;
|
|
1413
1718
|
}
|
|
1414
1719
|
}
|
|
1415
1720
|
else if (char === '"' || char === "'") {
|
|
@@ -1417,15 +1722,13 @@ function parseFilterArgs(argsText) {
|
|
|
1417
1722
|
hasQuote = true;
|
|
1418
1723
|
}
|
|
1419
1724
|
else if (char === ',') {
|
|
1420
|
-
|
|
1421
|
-
current = '';
|
|
1422
|
-
hasQuote = false;
|
|
1725
|
+
flush();
|
|
1423
1726
|
}
|
|
1424
1727
|
else {
|
|
1425
1728
|
current += char;
|
|
1426
1729
|
}
|
|
1427
1730
|
}
|
|
1428
|
-
const last = current
|
|
1731
|
+
const last = finalizeArg(current, firstQuoteStart, lastQuoteEnd);
|
|
1429
1732
|
if (last || hasQuote) {
|
|
1430
1733
|
args.push(last);
|
|
1431
1734
|
}
|
|
@@ -1578,11 +1881,11 @@ function parseBindTextsForElement(bindText) {
|
|
|
1578
1881
|
}
|
|
1579
1882
|
const propPart = bindText.slice(0, separatorIndex).trim();
|
|
1580
1883
|
const statePart = bindText.slice(separatorIndex + 1).trim();
|
|
1581
|
-
if (propPart ===
|
|
1884
|
+
if (propPart === ELSE_KEYWORD) {
|
|
1582
1885
|
const pathInfo = getPathInfo('#else');
|
|
1583
1886
|
return {
|
|
1584
|
-
propName:
|
|
1585
|
-
propSegments: [
|
|
1887
|
+
propName: ELSE_KEYWORD,
|
|
1888
|
+
propSegments: [ELSE_KEYWORD],
|
|
1586
1889
|
propModifiers: [],
|
|
1587
1890
|
statePathName: '#else',
|
|
1588
1891
|
statePathInfo: pathInfo,
|
|
@@ -1592,7 +1895,7 @@ function parseBindTextsForElement(bindText) {
|
|
|
1592
1895
|
bindingType: 'else',
|
|
1593
1896
|
};
|
|
1594
1897
|
}
|
|
1595
|
-
else if (propPart ===
|
|
1898
|
+
else if (propPart === SPREAD_PROP) {
|
|
1596
1899
|
const stateResult = parseStatePart(statePart);
|
|
1597
1900
|
if (stateResult.outFilters.length > 0) {
|
|
1598
1901
|
raiseError(`Invalid spread binding "${bindText}": filters are not allowed on spread targets.`);
|
|
@@ -1601,8 +1904,8 @@ function parseBindTextsForElement(bindText) {
|
|
|
1601
1904
|
raiseError(`Invalid spread binding "${bindText}": spread target path is required.`);
|
|
1602
1905
|
}
|
|
1603
1906
|
return {
|
|
1604
|
-
propName:
|
|
1605
|
-
propSegments: [
|
|
1907
|
+
propName: SPREAD_PROP,
|
|
1908
|
+
propSegments: [SPREAD_PROP],
|
|
1606
1909
|
propModifiers: [],
|
|
1607
1910
|
inFilters: [],
|
|
1608
1911
|
...stateResult,
|
|
@@ -1629,14 +1932,14 @@ function parseBindTextsForElement(bindText) {
|
|
|
1629
1932
|
const propResult = parsePropPart(propPart);
|
|
1630
1933
|
// eventToken.<prop>: <name> は要素 dispatch を state へ流す pub/sub 配線。
|
|
1631
1934
|
// 値適用ではないため bindingType 'event' として listener attach 経路に乗せる。
|
|
1632
|
-
if (propResult.propSegments[0] ===
|
|
1935
|
+
if (propResult.propSegments[0] === EVENT_TOKEN_NAMESPACE) {
|
|
1633
1936
|
return {
|
|
1634
1937
|
...propResult,
|
|
1635
1938
|
...stateResult,
|
|
1636
1939
|
bindingType: 'event',
|
|
1637
1940
|
};
|
|
1638
1941
|
}
|
|
1639
|
-
if (propResult.propSegments[0].startsWith(
|
|
1942
|
+
if (propResult.propSegments[0].startsWith(EVENT_PROP_PREFIX)) {
|
|
1640
1943
|
return {
|
|
1641
1944
|
...propResult,
|
|
1642
1945
|
...stateResult,
|
|
@@ -1656,7 +1959,9 @@ function parseBindTextsForElement(bindText) {
|
|
|
1656
1959
|
if (results.length > 1) {
|
|
1657
1960
|
const isIncludeSingleBinding = results.some(r => STRUCTURAL_BINDING_TYPE_SET.has(r.bindingType));
|
|
1658
1961
|
if (isIncludeSingleBinding) {
|
|
1659
|
-
|
|
1962
|
+
// LINT_HINT は付けない: 単独バインディング検査は lint 側に未実装で、誘導が
|
|
1963
|
+
// 空振りする(lint への検査追加は follow-up)。
|
|
1964
|
+
raiseError(`[wcs/template-syntax] Invalid bindText: "${bindText}". 'if', 'elseif', 'else', and 'for' bindings must be single binding. Put the structural binding alone in its own data-wcs (e.g. <template data-wcs="for: items">).`);
|
|
1660
1965
|
}
|
|
1661
1966
|
}
|
|
1662
1967
|
return results;
|
|
@@ -1778,13 +2083,97 @@ function getParseBindTextResults(node) {
|
|
|
1778
2083
|
return [];
|
|
1779
2084
|
}
|
|
1780
2085
|
|
|
2086
|
+
/**
|
|
2087
|
+
* bindings/lightDomComponentScope.ts — Light DOM の mapped `bind-component` を
|
|
2088
|
+
* 「ホストとは別のバインディングスコープ」として扱うための判定
|
|
2089
|
+
* (docs/architecture-hardening/15-state-component-mechanism-consistency.md §1.13)。
|
|
2090
|
+
*
|
|
2091
|
+
* Shadow DOM 形では、コンポーネントの `<wcs-state>` が**別 rootNode** にいることで
|
|
2092
|
+
* 2 つのことが同時に成立している。
|
|
2093
|
+
*
|
|
2094
|
+
* 1. ホスト root の `waitForStateInitialize` の走査集合に入らない
|
|
2095
|
+
* 2. 子スコープのバインディングがホストとは別の `buildBindings` パスで処理される
|
|
2096
|
+
*
|
|
2097
|
+
* Light DOM では両方が失われる。1 が失われると、
|
|
2098
|
+
* 「ホストの `waitForStateInitialize` が子 state を待つ →
|
|
2099
|
+
* 子 state は自分を束ねるホスト binding を待つ →
|
|
2100
|
+
* その binding を作る `initializeBindings` は `waitForStateInitialize` の後」
|
|
2101
|
+
* という循環になり、初期化が永久に解決しない。2 が失われると、子スコープの
|
|
2102
|
+
* `@name` 参照がホストと同じパスで解決されてしまい、子 state の名前登録より
|
|
2103
|
+
* 先に評価される。
|
|
2104
|
+
*
|
|
2105
|
+
* このモジュールはその 2 つを明示的に復元するための判定だけを持つ。
|
|
2106
|
+
*
|
|
2107
|
+
* **plain(ホストからバインドしない state 注入)は対象外**であることに注意。
|
|
2108
|
+
* plain は `waitInitializeBinding` を通らないので循環せず、従来どおりホストと
|
|
2109
|
+
* 同じパスで初期化して問題ない。ここで一律に切り出すと、成立している plain 形が
|
|
2110
|
+
* 「子 state の登録前に `@name` を解決する」形に退行する。
|
|
2111
|
+
*/
|
|
2112
|
+
/** `<wcs-state bind-component>` が Light DOM の mapped 形(=別スコープ扱い)か。 */
|
|
2113
|
+
function isLightDomMappedStateElement(stateElement) {
|
|
2114
|
+
if (!stateElement.hasAttribute("bind-component")) {
|
|
2115
|
+
return false;
|
|
2116
|
+
}
|
|
2117
|
+
const parentNode = stateElement.parentNode;
|
|
2118
|
+
// Shadow DOM 形では parentNode が ShadowRoot になる(かつホスト root の
|
|
2119
|
+
// querySelectorAll にはそもそも出てこない)
|
|
2120
|
+
if (!(parentNode instanceof Element)) {
|
|
2121
|
+
return false;
|
|
2122
|
+
}
|
|
2123
|
+
// ホストからバインドされていなければ plain。従来どおりの扱いに任せる
|
|
2124
|
+
return parentNode.hasAttribute(config.bindAttributeName);
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* `root` の内側にある Light DOM mapped コンポーネント要素を集める。
|
|
2128
|
+
*
|
|
2129
|
+
* `root` 自身は**含めない**。子スコープが自分のパスとして
|
|
2130
|
+
* `initializeBindings(componentElement)` を呼ぶとき、その要素自身まで prune すると
|
|
2131
|
+
* 何も初期化されなくなるため。
|
|
2132
|
+
*/
|
|
2133
|
+
function findNestedLightDomComponents(root) {
|
|
2134
|
+
const components = [];
|
|
2135
|
+
const stateElements = root.querySelectorAll(`${config.tagNames.state}[bind-component]`);
|
|
2136
|
+
for (const stateElement of stateElements) {
|
|
2137
|
+
if (!isLightDomMappedStateElement(stateElement)) {
|
|
2138
|
+
continue;
|
|
2139
|
+
}
|
|
2140
|
+
const component = stateElement.parentNode;
|
|
2141
|
+
if (component === root) {
|
|
2142
|
+
continue;
|
|
2143
|
+
}
|
|
2144
|
+
components.push(component);
|
|
2145
|
+
}
|
|
2146
|
+
return components;
|
|
2147
|
+
}
|
|
2148
|
+
/** `node` が、いずれかのコンポーネント要素の**真の**子孫か。 */
|
|
2149
|
+
function isInsideAnyComponent(node, components) {
|
|
2150
|
+
for (let i = 0; i < components.length; i++) {
|
|
2151
|
+
const component = components[i];
|
|
2152
|
+
if (component !== node && component.contains(node)) {
|
|
2153
|
+
return true;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
return false;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
1781
2159
|
/**
|
|
1782
2160
|
* data-wcs 属性または埋め込みノード<!--{{}}-->を持つノードをすべて取得する
|
|
2161
|
+
*
|
|
2162
|
+
* Light DOM の mapped コンポーネントの**内側**は除外する(§1.13)。そのサブツリーの
|
|
2163
|
+
* `@name` 参照は、コンポーネント側の state が名前登録を済ませてからでないと解決できず、
|
|
2164
|
+
* ホストと同じパスで拾うと登録前に評価されてしまう。除外したぶんは、その state が
|
|
2165
|
+
* 初期化を終えた時点で自分のスコープとして `initializeBindings(componentElement)` を
|
|
2166
|
+
* 呼び直す(Shadow DOM 形で rootNode ごとにパスが分かれるのと同じ形にする)。
|
|
2167
|
+
*
|
|
2168
|
+
* コンポーネント要素**自身**は除外しない。ホスト側の `data-wcs`(`state.msg: user.name`)
|
|
2169
|
+
* はホストのスコープに属し、それが張られることで子側の待ちが解ける。
|
|
2170
|
+
*
|
|
1783
2171
|
* @param root
|
|
1784
2172
|
* @returns
|
|
1785
2173
|
*/
|
|
1786
2174
|
function getSubscriberNodes(root) {
|
|
1787
2175
|
const subscriberNodes = [];
|
|
2176
|
+
const nestedComponents = findNestedLightDomComponents(root);
|
|
1788
2177
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT, {
|
|
1789
2178
|
acceptNode(node) {
|
|
1790
2179
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
@@ -1803,7 +2192,14 @@ function getSubscriberNodes(root) {
|
|
|
1803
2192
|
}
|
|
1804
2193
|
});
|
|
1805
2194
|
while (walker.nextNode()) {
|
|
1806
|
-
|
|
2195
|
+
const node = walker.currentNode;
|
|
2196
|
+
// TreeWalker の acceptNode は「自分は拾うが子孫は辿らない」を表現できないため、
|
|
2197
|
+
// コンポーネント要素自身を拾ったうえで、その子孫をここで落とす。
|
|
2198
|
+
// nestedComponents が空(=圧倒的多数)のときは contains 走査ごと発生しない。
|
|
2199
|
+
if (nestedComponents.length > 0 && isInsideAnyComponent(node, nestedComponents)) {
|
|
2200
|
+
continue;
|
|
2201
|
+
}
|
|
2202
|
+
subscriberNodes.push(node);
|
|
1807
2203
|
}
|
|
1808
2204
|
return subscriberNodes;
|
|
1809
2205
|
}
|
|
@@ -2441,8 +2837,8 @@ function getHandlerKey$3(binding, eventName) {
|
|
|
2441
2837
|
function getEventName$2(binding) {
|
|
2442
2838
|
let eventName = 'input';
|
|
2443
2839
|
for (const modifier of binding.propModifiers) {
|
|
2444
|
-
if (modifier.startsWith(
|
|
2445
|
-
eventName = modifier.slice(
|
|
2840
|
+
if (modifier.startsWith(EVENT_PROP_PREFIX)) {
|
|
2841
|
+
eventName = modifier.slice(EVENT_PROP_PREFIX.length);
|
|
2446
2842
|
}
|
|
2447
2843
|
}
|
|
2448
2844
|
return eventName;
|
|
@@ -2497,7 +2893,7 @@ const checkboxEventHandlerFunction = (stateName, statePathName, inFilters) => (e
|
|
|
2497
2893
|
});
|
|
2498
2894
|
};
|
|
2499
2895
|
function attachCheckboxEventHandler(binding) {
|
|
2500
|
-
if (binding.bindingType === "checkbox" && binding.propModifiers.indexOf(
|
|
2896
|
+
if (binding.bindingType === "checkbox" && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
2501
2897
|
const eventName = getEventName$2(binding);
|
|
2502
2898
|
const key = getHandlerKey$3(binding, eventName);
|
|
2503
2899
|
let checkboxEventHandler = handlerByHandlerKey$3.get(key);
|
|
@@ -2512,7 +2908,7 @@ function attachCheckboxEventHandler(binding) {
|
|
|
2512
2908
|
return false;
|
|
2513
2909
|
}
|
|
2514
2910
|
function detachCheckboxEventHandler(binding) {
|
|
2515
|
-
if (binding.bindingType === "checkbox" && binding.propModifiers.indexOf(
|
|
2911
|
+
if (binding.bindingType === "checkbox" && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
2516
2912
|
const eventName = getEventName$2(binding);
|
|
2517
2913
|
const key = getHandlerKey$3(binding, eventName);
|
|
2518
2914
|
const checkboxEventHandler = handlerByHandlerKey$3.get(key);
|
|
@@ -2642,12 +3038,12 @@ class EventToken extends Token {
|
|
|
2642
3038
|
}
|
|
2643
3039
|
}
|
|
2644
3040
|
|
|
2645
|
-
const registryByStateElement$
|
|
3041
|
+
const registryByStateElement$3 = new WeakMap();
|
|
2646
3042
|
function getOrCreateEventToken(stateElement, name) {
|
|
2647
|
-
let registry = registryByStateElement$
|
|
3043
|
+
let registry = registryByStateElement$3.get(stateElement);
|
|
2648
3044
|
if (typeof registry === "undefined") {
|
|
2649
3045
|
registry = new Map();
|
|
2650
|
-
registryByStateElement$
|
|
3046
|
+
registryByStateElement$3.set(stateElement, registry);
|
|
2651
3047
|
}
|
|
2652
3048
|
let token = registry.get(name);
|
|
2653
3049
|
if (typeof token === "undefined") {
|
|
@@ -2657,7 +3053,7 @@ function getOrCreateEventToken(stateElement, name) {
|
|
|
2657
3053
|
return token;
|
|
2658
3054
|
}
|
|
2659
3055
|
function clearEventTokenRegistry(stateElement) {
|
|
2660
|
-
registryByStateElement$
|
|
3056
|
+
registryByStateElement$3.delete(stateElement);
|
|
2661
3057
|
}
|
|
2662
3058
|
|
|
2663
3059
|
/**
|
|
@@ -2695,7 +3091,7 @@ function getWcBindable$1(element) {
|
|
|
2695
3091
|
return readBindableDeclaration(element);
|
|
2696
3092
|
}
|
|
2697
3093
|
function attachEventTokenHandler(binding) {
|
|
2698
|
-
if (binding.propSegments[0] !==
|
|
3094
|
+
if (binding.propSegments[0] !== EVENT_TOKEN_NAMESPACE) {
|
|
2699
3095
|
return false;
|
|
2700
3096
|
}
|
|
2701
3097
|
const element = binding.node;
|
|
@@ -2722,16 +3118,16 @@ function attachEventTokenHandler(binding) {
|
|
|
2722
3118
|
}
|
|
2723
3119
|
const propDesc = bindable.knownProperties.get(propertyName);
|
|
2724
3120
|
if (typeof propDesc === "undefined") {
|
|
2725
|
-
raiseError(`Property "${propertyName}" is not declared in wcBindable.properties of <${element.tagName.toLowerCase()}
|
|
3121
|
+
raiseError(`Property "${propertyName}" is not declared in wcBindable.properties of <${element.tagName.toLowerCase()}>.${didYouMean(propertyName, bindable.knownProperties.keys())}`);
|
|
2726
3122
|
}
|
|
2727
3123
|
const eventName = propDesc.event;
|
|
2728
3124
|
const tokenName = binding.statePathName;
|
|
2729
3125
|
const stateName = binding.stateName;
|
|
2730
3126
|
const modifiers = binding.propModifiers;
|
|
2731
3127
|
const handler = (event) => {
|
|
2732
|
-
if (modifiers.includes(
|
|
3128
|
+
if (modifiers.includes(MODIFIER_PREVENT))
|
|
2733
3129
|
event.preventDefault();
|
|
2734
|
-
if (modifiers.includes(
|
|
3130
|
+
if (modifiers.includes(MODIFIER_STOP))
|
|
2735
3131
|
event.stopPropagation();
|
|
2736
3132
|
// state は発火時の live root から解決する(attach 時は detached の可能性があるため)。
|
|
2737
3133
|
const rootNode = element.getRootNode();
|
|
@@ -2740,7 +3136,8 @@ function attachEventTokenHandler(binding) {
|
|
|
2740
3136
|
raiseError(`State element with name "${stateName}" not found for eventToken handler.`);
|
|
2741
3137
|
}
|
|
2742
3138
|
if (!stateElement.eventTokenNames.has(tokenName)) {
|
|
2743
|
-
|
|
3139
|
+
// lint も同じケースを wcs/token-undeclared で検出する(三面同語彙)。
|
|
3140
|
+
raiseError(`[wcs/token-undeclared] eventToken "${tokenName}" is not declared in $eventTokens of state "${stateName}".${didYouMean(tokenName, stateElement.eventTokenNames)}${LINT_HINT}`);
|
|
2744
3141
|
}
|
|
2745
3142
|
const loopContext = getLoopContextByNode(element);
|
|
2746
3143
|
stateElement.createStateAsync("writable", async (state) => {
|
|
@@ -2760,7 +3157,7 @@ function attachEventTokenHandler(binding) {
|
|
|
2760
3157
|
return true;
|
|
2761
3158
|
}
|
|
2762
3159
|
function detachEventTokenHandler(binding) {
|
|
2763
|
-
if (binding.propSegments[0] !==
|
|
3160
|
+
if (binding.propSegments[0] !== EVENT_TOKEN_NAMESPACE) {
|
|
2764
3161
|
return false;
|
|
2765
3162
|
}
|
|
2766
3163
|
const listener = listenerByBinding.get(binding);
|
|
@@ -2813,13 +3210,13 @@ const handlerByHandlerKey$2 = new Map();
|
|
|
2813
3210
|
// binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
|
|
2814
3211
|
const bindingRegistry$2 = createHandlerBindingRegistry();
|
|
2815
3212
|
function getHandlerKey$2(binding) {
|
|
2816
|
-
const modifierKey = binding.propModifiers.filter(m => m ===
|
|
3213
|
+
const modifierKey = binding.propModifiers.filter(m => m === MODIFIER_PREVENT || m === MODIFIER_STOP).sort().join(',');
|
|
2817
3214
|
return `${binding.stateName}::${binding.statePathName}::${modifierKey}`;
|
|
2818
3215
|
}
|
|
2819
3216
|
const stateEventHandlerFunction = (stateName, handlerName, modifiers, statePathInfo) => (event) => {
|
|
2820
|
-
if (modifiers.includes(
|
|
3217
|
+
if (modifiers.includes(MODIFIER_PREVENT))
|
|
2821
3218
|
event.preventDefault();
|
|
2822
|
-
if (modifiers.includes(
|
|
3219
|
+
if (modifiers.includes(MODIFIER_STOP))
|
|
2823
3220
|
event.stopPropagation();
|
|
2824
3221
|
const node = event.target;
|
|
2825
3222
|
const rootNode = node.getRootNode();
|
|
@@ -2853,7 +3250,7 @@ const stateEventHandlerFunction = (stateName, handlerName, modifiers, statePathI
|
|
|
2853
3250
|
});
|
|
2854
3251
|
};
|
|
2855
3252
|
function attachEventHandler(binding) {
|
|
2856
|
-
if (!binding.propName.startsWith(
|
|
3253
|
+
if (!binding.propName.startsWith(EVENT_PROP_PREFIX)) {
|
|
2857
3254
|
return false;
|
|
2858
3255
|
}
|
|
2859
3256
|
const key = getHandlerKey$2(binding);
|
|
@@ -2868,7 +3265,7 @@ function attachEventHandler(binding) {
|
|
|
2868
3265
|
return true;
|
|
2869
3266
|
}
|
|
2870
3267
|
function detachEventHandler(binding) {
|
|
2871
|
-
if (!binding.propName.startsWith(
|
|
3268
|
+
if (!binding.propName.startsWith(EVENT_PROP_PREFIX)) {
|
|
2872
3269
|
return false;
|
|
2873
3270
|
}
|
|
2874
3271
|
const key = getHandlerKey$2(binding);
|
|
@@ -2897,8 +3294,8 @@ function getHandlerKey$1(binding, eventName) {
|
|
|
2897
3294
|
function getEventName$1(binding) {
|
|
2898
3295
|
let eventName = 'input';
|
|
2899
3296
|
for (const modifier of binding.propModifiers) {
|
|
2900
|
-
if (modifier.startsWith(
|
|
2901
|
-
eventName = modifier.slice(
|
|
3297
|
+
if (modifier.startsWith(EVENT_PROP_PREFIX)) {
|
|
3298
|
+
eventName = modifier.slice(EVENT_PROP_PREFIX.length);
|
|
2902
3299
|
}
|
|
2903
3300
|
}
|
|
2904
3301
|
return eventName;
|
|
@@ -2934,7 +3331,7 @@ const radioEventHandlerFunction = (stateName, statePathName, inFilters) => (even
|
|
|
2934
3331
|
});
|
|
2935
3332
|
};
|
|
2936
3333
|
function attachRadioEventHandler(binding) {
|
|
2937
|
-
if (binding.bindingType === "radio" && binding.propModifiers.indexOf(
|
|
3334
|
+
if (binding.bindingType === "radio" && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
2938
3335
|
const eventName = getEventName$1(binding);
|
|
2939
3336
|
const key = getHandlerKey$1(binding, eventName);
|
|
2940
3337
|
let radioEventHandler = handlerByHandlerKey$1.get(key);
|
|
@@ -2949,7 +3346,7 @@ function attachRadioEventHandler(binding) {
|
|
|
2949
3346
|
return false;
|
|
2950
3347
|
}
|
|
2951
3348
|
function detachRadioEventHandler(binding) {
|
|
2952
|
-
if (binding.bindingType === "radio" && binding.propModifiers.indexOf(
|
|
3349
|
+
if (binding.bindingType === "radio" && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
2953
3350
|
const eventName = getEventName$1(binding);
|
|
2954
3351
|
const key = getHandlerKey$1(binding, eventName);
|
|
2955
3352
|
const radioEventHandler = handlerByHandlerKey$1.get(key);
|
|
@@ -3188,10 +3585,10 @@ function getEventName(binding) {
|
|
|
3188
3585
|
eventName = propDesc.event;
|
|
3189
3586
|
}
|
|
3190
3587
|
}
|
|
3191
|
-
// 3.modifier
|
|
3588
|
+
// 3.modifier(`#onchange` 等 — `on` + イベント名の修飾子形。README「Modifiers」参照)
|
|
3192
3589
|
for (const modifier of binding.propModifiers) {
|
|
3193
|
-
if (modifier.startsWith(
|
|
3194
|
-
eventName = modifier.slice(
|
|
3590
|
+
if (modifier.startsWith(EVENT_PROP_PREFIX)) {
|
|
3591
|
+
eventName = modifier.slice(EVENT_PROP_PREFIX.length);
|
|
3195
3592
|
}
|
|
3196
3593
|
}
|
|
3197
3594
|
return eventName;
|
|
@@ -3360,7 +3757,7 @@ function attachTwowayEventHandler(binding) {
|
|
|
3360
3757
|
return;
|
|
3361
3758
|
}
|
|
3362
3759
|
}
|
|
3363
|
-
if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf(
|
|
3760
|
+
if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
3364
3761
|
const eventName = getEventName(binding);
|
|
3365
3762
|
const valueGetter = getValueGetter(binding);
|
|
3366
3763
|
const isOccurrence = isOccurrenceProperty(binding);
|
|
@@ -3386,7 +3783,7 @@ function detachTwowayEventHandler(binding) {
|
|
|
3386
3783
|
return;
|
|
3387
3784
|
}
|
|
3388
3785
|
}
|
|
3389
|
-
if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf(
|
|
3786
|
+
if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
3390
3787
|
const eventName = getEventName(binding);
|
|
3391
3788
|
const valueGetter = getValueGetter(binding);
|
|
3392
3789
|
const key = getHandlerKey(binding, eventName, valueGetter !== null, isOccurrenceProperty(binding));
|
|
@@ -3401,6 +3798,42 @@ function detachTwowayEventHandler(binding) {
|
|
|
3401
3798
|
}
|
|
3402
3799
|
}
|
|
3403
3800
|
|
|
3801
|
+
const stateElementByWebComponent = new WeakMap();
|
|
3802
|
+
function setStateElementByWebComponent(webComponent, stateName, stateElement) {
|
|
3803
|
+
let stateMap = stateElementByWebComponent.get(webComponent);
|
|
3804
|
+
if (!stateMap) {
|
|
3805
|
+
stateMap = new Map();
|
|
3806
|
+
stateElementByWebComponent.set(webComponent, stateMap);
|
|
3807
|
+
}
|
|
3808
|
+
stateMap.set(stateName, stateElement);
|
|
3809
|
+
}
|
|
3810
|
+
function getStateElementByWebComponent(webComponent, stateName) {
|
|
3811
|
+
const stateMap = stateElementByWebComponent.get(webComponent);
|
|
3812
|
+
if (!stateMap) {
|
|
3813
|
+
return null;
|
|
3814
|
+
}
|
|
3815
|
+
return stateMap.get(stateName) ?? null;
|
|
3816
|
+
}
|
|
3817
|
+
/**
|
|
3818
|
+
* コンポーネントが mapped されている「1 つ外のスコープ」の state 要素。
|
|
3819
|
+
* `buildPrimaryMappingRule` がプライマリ規則から記録する
|
|
3820
|
+
* (規則の outer 側が属する state 要素 = 値の正本を持つスコープそのもの)。
|
|
3821
|
+
*
|
|
3822
|
+
* 用途は Δ(base listIndex)の境界越え合成(§1.12)。`getLoopContextByNode` は
|
|
3823
|
+
* `parentNode` しか辿らず shadow 境界を越えないため、Δ を外へ引き継ぐには
|
|
3824
|
+
* 「1 つ外のスコープ」への明示的なリンクが要る。
|
|
3825
|
+
*
|
|
3826
|
+
* この台帳を MappingRule ではなくここに置くのは循環参照を避けるため
|
|
3827
|
+
* (baseListIndex → MappingRule → BindingSession → outerListPath → baseListIndex)。
|
|
3828
|
+
*/
|
|
3829
|
+
const outerStateElementByWebComponent = new WeakMap();
|
|
3830
|
+
function setOuterStateElementByWebComponent(webComponent, stateElement) {
|
|
3831
|
+
outerStateElementByWebComponent.set(webComponent, stateElement);
|
|
3832
|
+
}
|
|
3833
|
+
function getOuterStateElementByWebComponent(webComponent) {
|
|
3834
|
+
return outerStateElementByWebComponent.get(webComponent) ?? null;
|
|
3835
|
+
}
|
|
3836
|
+
|
|
3404
3837
|
/**
|
|
3405
3838
|
* webComponent/baseListIndex.ts
|
|
3406
3839
|
*
|
|
@@ -3423,19 +3856,49 @@ function detachTwowayEventHandler(binding) {
|
|
|
3423
3856
|
* ホットパスに walk は載らない。
|
|
3424
3857
|
*/
|
|
3425
3858
|
function getBaseListIndex(stateElement) {
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3859
|
+
let current = stateElement;
|
|
3860
|
+
for (;;) {
|
|
3861
|
+
if (current == null || current.hasMappedComponentState !== true) {
|
|
3862
|
+
return null;
|
|
3863
|
+
}
|
|
3864
|
+
const component = current.boundComponent;
|
|
3865
|
+
if (component == null) {
|
|
3866
|
+
return null;
|
|
3867
|
+
}
|
|
3868
|
+
const listIndex = getLoopContextByNode(component)?.listIndex;
|
|
3869
|
+
if (listIndex != null) {
|
|
3870
|
+
return listIndex;
|
|
3871
|
+
}
|
|
3872
|
+
// このスコープには囲むループが無い。コンポーネントがさらに別の mapped な
|
|
3873
|
+
// コンポーネントの shadow の中にいるなら、Δ は外側スコープから引き継ぐ(§1.12)。
|
|
3874
|
+
//
|
|
3875
|
+
// `getLoopContextByNode` は `parentNode` しか辿らず、ShadowRoot の parentNode は
|
|
3876
|
+
// null なので shadow 境界で必ず止まる。境界 1 枚なら外側は素の文書スコープで
|
|
3877
|
+
// Δ=0 が正しいが、2 枚重なっていると中間スコープの Δ が丸ごと落ちて
|
|
3878
|
+
// 子の listIndex が正本スコープより浅い arity で作られる。
|
|
3879
|
+
//
|
|
3880
|
+
// 外側が mapped でない(=値の正本がそのスコープにある)なら、そこから先の
|
|
3881
|
+
// ループはこの子のリストとは無関係なので次の周回の先頭ガードで止まる。
|
|
3882
|
+
current = getOuterStateElementByWebComponent(component);
|
|
3432
3883
|
}
|
|
3433
|
-
return getLoopContextByNode(component)?.listIndex ?? null;
|
|
3434
3884
|
}
|
|
3435
3885
|
/** base の段数 Δ。base が無ければ 0。 */
|
|
3436
3886
|
function getBaseDepth(stateElement) {
|
|
3437
3887
|
return getBaseListIndex(stateElement)?.length ?? 0;
|
|
3438
3888
|
}
|
|
3889
|
+
/**
|
|
3890
|
+
* そのスコープでそのパスに実際に使われる listIndex の arity。
|
|
3891
|
+
*
|
|
3892
|
+
* パス自身のワイルドカード段数に、そのスコープが外側のループの内側にいる分(Δ)を
|
|
3893
|
+
* 足したもの。`items.*` は子スコープから見れば 1 段でも、そのスコープが Δ=1 の位置に
|
|
3894
|
+
* あれば台帳の listIndex は arity 2 になる(§1.10)。
|
|
3895
|
+
*
|
|
3896
|
+
* **境界を跨ぐ照合はこの実 arity どうしで行うこと**(§1.12)。片側だけ Δ を足すと、
|
|
3897
|
+
* 境界が 2 枚以上あるときに中間スコープの Δ を二重計上して不一致になる。
|
|
3898
|
+
*/
|
|
3899
|
+
function getScopeArity(stateElement, pathInfo) {
|
|
3900
|
+
return pathInfo.wildcardCount + getBaseDepth(stateElement);
|
|
3901
|
+
}
|
|
3439
3902
|
/**
|
|
3440
3903
|
* リストの行を生成するときの親 listIndex。
|
|
3441
3904
|
*
|
|
@@ -3451,23 +3914,6 @@ function getListParentListIndex(stateElement, containerListIndex) {
|
|
|
3451
3914
|
return containerListIndex ?? getBaseListIndex(stateElement);
|
|
3452
3915
|
}
|
|
3453
3916
|
|
|
3454
|
-
const stateElementByWebComponent = new WeakMap();
|
|
3455
|
-
function setStateElementByWebComponent(webComponent, stateName, stateElement) {
|
|
3456
|
-
let stateMap = stateElementByWebComponent.get(webComponent);
|
|
3457
|
-
if (!stateMap) {
|
|
3458
|
-
stateMap = new Map();
|
|
3459
|
-
stateElementByWebComponent.set(webComponent, stateMap);
|
|
3460
|
-
}
|
|
3461
|
-
stateMap.set(stateName, stateElement);
|
|
3462
|
-
}
|
|
3463
|
-
function getStateElementByWebComponent(webComponent, stateName) {
|
|
3464
|
-
const stateMap = stateElementByWebComponent.get(webComponent);
|
|
3465
|
-
if (!stateMap) {
|
|
3466
|
-
return null;
|
|
3467
|
-
}
|
|
3468
|
-
return stateMap.get(stateName) ?? null;
|
|
3469
|
-
}
|
|
3470
|
-
|
|
3471
3917
|
const innerMappingByElement = new WeakMap();
|
|
3472
3918
|
const outerMappingByElement = new WeakMap();
|
|
3473
3919
|
const primaryMappingRuleSetByElement = new WeakMap();
|
|
@@ -3503,6 +3949,10 @@ function buildPrimaryMappingRule(webComponent, stateName, bindings) {
|
|
|
3503
3949
|
primaryBindingByMappingRule.set(mappingRule, binding);
|
|
3504
3950
|
innerMappingRule.set(innerAbsPathInfo, outerAbsPathInfo);
|
|
3505
3951
|
outerMappingRule.set(outerAbsPathInfo, innerAbsPathInfo);
|
|
3952
|
+
// 1 つ外のスコープへのリンク。Δ の境界越え合成(§1.12)が引く。
|
|
3953
|
+
// プライマリ規則はすべて同じホスト要素の data-wcs 由来なので、どの規則から
|
|
3954
|
+
// 採っても同じスコープを指す。
|
|
3955
|
+
setOuterStateElementByWebComponent(webComponent, outerAbsPathInfo.stateElement);
|
|
3506
3956
|
}
|
|
3507
3957
|
innerMappingByElement.set(webComponent, innerMappingRule);
|
|
3508
3958
|
outerMappingByElement.set(webComponent, outerMappingRule);
|
|
@@ -3728,12 +4178,56 @@ function getOuterRowPathInfo(innerStateElement, innerPathInfo) {
|
|
|
3728
4178
|
if (innerPathInfo.wildcardCount === 0) {
|
|
3729
4179
|
return null;
|
|
3730
4180
|
}
|
|
4181
|
+
return stepOuterRowPathInfo(innerStateElement, innerPathInfo);
|
|
4182
|
+
}
|
|
4183
|
+
/**
|
|
4184
|
+
* `getOuterRowPathInfo` の 2 段目以降。境界が 2 枚以上重なっている(コンポーネントの
|
|
4185
|
+
* shadow の中にさらに mapped な `bind-component` がある)場合、値の正本は 1 つ外では
|
|
4186
|
+
* なく**最も外のスコープ**にある。1 段目だけに相乗りしていると、中間スコープは
|
|
4187
|
+
* 素通しで自分の行バインディングを持たないため、正本スコープ起点の行フィールド
|
|
4188
|
+
* 書き込みを購読する者が誰もいなくなる(§1.11)。
|
|
4189
|
+
*
|
|
4190
|
+
* 1 段目が成立したときだけ呼ばれる(=mapped な行バインディング限定)ので、
|
|
4191
|
+
* 通常のリストはこの walk を一切踏まない。返り値は 2 段目以降が無ければ `null` で、
|
|
4192
|
+
* 圧倒的多数である深さ 1 の行では配列を確保しない。
|
|
4193
|
+
*
|
|
4194
|
+
* 各段で必ず外側の state 要素へ進む(`resolveOuterAbsolutePathInfo` は
|
|
4195
|
+
* `boundComponent` の属するスコープを返す=DOM 上の真の祖先)ので停止する。
|
|
4196
|
+
* `propagateListPathToOuterState` の外向き伝播と同じ論拠。
|
|
4197
|
+
*/
|
|
4198
|
+
function getOuterRowPathInfosBeyond(firstOuterAbsPathInfo) {
|
|
4199
|
+
let rest = null;
|
|
4200
|
+
let stateElement = firstOuterAbsPathInfo.stateElement;
|
|
4201
|
+
let pathInfo = firstOuterAbsPathInfo.pathInfo;
|
|
4202
|
+
for (;;) {
|
|
4203
|
+
const outerAbsPathInfo = stepOuterRowPathInfo(stateElement, pathInfo);
|
|
4204
|
+
if (outerAbsPathInfo === null) {
|
|
4205
|
+
return rest;
|
|
4206
|
+
}
|
|
4207
|
+
(rest ??= []).push(outerAbsPathInfo);
|
|
4208
|
+
stateElement = outerAbsPathInfo.stateElement;
|
|
4209
|
+
pathInfo = outerAbsPathInfo.pathInfo;
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
/**
|
|
4213
|
+
* 境界 1 枚分の外向き解決。成立条件の判定を含む。
|
|
4214
|
+
*
|
|
4215
|
+
* 判定は**両側の実 arity(`getScopeArity` = パスの段数 + そのスコープの Δ)が
|
|
4216
|
+
* 一致すること**。相乗り登録は子の listIndex をそのまま鍵に使うので、外側スコープが
|
|
4217
|
+
* その arity で台帳を引けなければ意味がない。
|
|
4218
|
+
*
|
|
4219
|
+
* 境界 1 枚なら外側は Δ=0 なので、これは従来の `Δ + innerW === outerW` と同値。
|
|
4220
|
+
* 2 枚以上あるときに外側の Δ を数えないと、中間スコープの Δ を二重計上して
|
|
4221
|
+
* 成立するはずの段を落とす(§1.12)。
|
|
4222
|
+
*/
|
|
4223
|
+
function stepOuterRowPathInfo(innerStateElement, innerPathInfo) {
|
|
3731
4224
|
const outerAbsPathInfo = resolveOuterAbsolutePathInfo(innerStateElement, innerPathInfo);
|
|
3732
4225
|
if (outerAbsPathInfo === null || outerAbsPathInfo.stateElement === innerStateElement) {
|
|
3733
4226
|
return null;
|
|
3734
4227
|
}
|
|
3735
|
-
const
|
|
3736
|
-
|
|
4228
|
+
const innerArity = getScopeArity(innerStateElement, innerPathInfo);
|
|
4229
|
+
const outerArity = getScopeArity(outerAbsPathInfo.stateElement, outerAbsPathInfo.pathInfo);
|
|
4230
|
+
if (innerArity !== outerArity) {
|
|
3737
4231
|
return null;
|
|
3738
4232
|
}
|
|
3739
4233
|
return outerAbsPathInfo;
|
|
@@ -3877,7 +4371,7 @@ function readOption(binding, key) {
|
|
|
3877
4371
|
continue;
|
|
3878
4372
|
const modifierKey = modifier.slice(0, separator).trim();
|
|
3879
4373
|
const value = modifier.slice(separator + 1).trim();
|
|
3880
|
-
if (modifierKey !==
|
|
4374
|
+
if (modifierKey !== MODIFIER_KEY_INIT && modifierKey !== MODIFIER_KEY_SYNC) {
|
|
3881
4375
|
raiseError(`Unknown binding modifier "${modifierKey}" in "${modifier}".`);
|
|
3882
4376
|
}
|
|
3883
4377
|
if (modifierKey !== key)
|
|
@@ -3924,8 +4418,8 @@ function resolveInitialSyncPolicy(binding) {
|
|
|
3924
4418
|
}
|
|
3925
4419
|
return STATE_CALL_POLICY;
|
|
3926
4420
|
}
|
|
3927
|
-
const explicitAuthority = parseAuthority(readOption(binding,
|
|
3928
|
-
const syncOn = parseSyncOn(readOption(binding,
|
|
4421
|
+
const explicitAuthority = parseAuthority(readOption(binding, MODIFIER_KEY_INIT));
|
|
4422
|
+
const syncOn = parseSyncOn(readOption(binding, MODIFIER_KEY_SYNC));
|
|
3929
4423
|
if (binding.bindingType === "event") {
|
|
3930
4424
|
if (explicitAuthority !== null && explicitAuthority !== "none") {
|
|
3931
4425
|
raiseError("Event bindings only allow init=none.");
|
|
@@ -3937,7 +4431,7 @@ function resolveInitialSyncPolicy(binding) {
|
|
|
3937
4431
|
// property authority 検証(未宣言なら raiseError)に掛けてはならない。値の初期同期を
|
|
3938
4432
|
// 持たない配線なので、現行互換の "state" authority を返す(command token は従来通り
|
|
3939
4433
|
// 初期 apply で配線される)。
|
|
3940
|
-
if (binding.propSegments[0] ===
|
|
4434
|
+
if (binding.propSegments[0] === COMMAND_NAMESPACE) {
|
|
3941
4435
|
return statePolicy("state", syncOn);
|
|
3942
4436
|
}
|
|
3943
4437
|
if (binding.bindingType !== "prop") {
|
|
@@ -4552,6 +5046,7 @@ class BindingSession {
|
|
|
4552
5046
|
patternPathInfo: null,
|
|
4553
5047
|
patternListIndex: null,
|
|
4554
5048
|
outerPatternPathInfo: null,
|
|
5049
|
+
outerPatternPathInfosRest: null,
|
|
4555
5050
|
pendingDefinitions: 0,
|
|
4556
5051
|
initialPolicy: slot.policy,
|
|
4557
5052
|
resolvedAuthority: slot.authority,
|
|
@@ -4668,6 +5163,7 @@ class BindingSession {
|
|
|
4668
5163
|
patternPathInfo: null,
|
|
4669
5164
|
patternListIndex: null,
|
|
4670
5165
|
outerPatternPathInfo: null,
|
|
5166
|
+
outerPatternPathInfosRest: null,
|
|
4671
5167
|
pendingDefinitions: 0,
|
|
4672
5168
|
initialPolicy: null,
|
|
4673
5169
|
resolvedAuthority: null,
|
|
@@ -4708,7 +5204,7 @@ class BindingSession {
|
|
|
4708
5204
|
record.eventAttached = true;
|
|
4709
5205
|
return;
|
|
4710
5206
|
}
|
|
4711
|
-
if (binding.propSegments[0] ===
|
|
5207
|
+
if (binding.propSegments[0] === EVENT_TOKEN_NAMESPACE) {
|
|
4712
5208
|
this.attachAfterDefinition(record, () => {
|
|
4713
5209
|
if (attachEventTokenHandler(binding)) {
|
|
4714
5210
|
addRecordTeardown(record, () => detachEventTokenHandler(binding));
|
|
@@ -4734,7 +5230,7 @@ class BindingSession {
|
|
|
4734
5230
|
// isPossibleTwoWay の未定義 CE raiseError も踏まない)。
|
|
4735
5231
|
if (config.enableDirectionalInitialSync
|
|
4736
5232
|
&& isPossibleTwoWay(binding.node, binding.propName)
|
|
4737
|
-
&& binding.propModifiers.indexOf(
|
|
5233
|
+
&& binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
4738
5234
|
const removeObserver = addTwowayValueObserver(binding.node, binding.propName, (value) => {
|
|
4739
5235
|
if (!this.isAlive(record, record.generation))
|
|
4740
5236
|
return;
|
|
@@ -4884,6 +5380,17 @@ class BindingSession {
|
|
|
4884
5380
|
if (outerPathInfo !== null) {
|
|
4885
5381
|
addBindingByPattern(outerPathInfo, listIndex, binding);
|
|
4886
5382
|
record.outerPatternPathInfo = outerPathInfo;
|
|
5383
|
+
// 境界が 2 枚以上重なっていると、値の正本は 1 つ外ではなく最も外のスコープに
|
|
5384
|
+
// ある。中間スコープは配列を素通しするだけで自分の行バインディングを持たない
|
|
5385
|
+
// ため、1 段目だけでは正本スコープ起点の行フィールド書き込みが誰にも届かない
|
|
5386
|
+
// (§1.11)。成立する段すべてに載せる。
|
|
5387
|
+
const restPathInfos = getOuterRowPathInfosBeyond(outerPathInfo);
|
|
5388
|
+
if (restPathInfos !== null) {
|
|
5389
|
+
for (let i = 0; i < restPathInfos.length; i++) {
|
|
5390
|
+
addBindingByPattern(restPathInfos[i], listIndex, binding);
|
|
5391
|
+
}
|
|
5392
|
+
record.outerPatternPathInfosRest = restPathInfos;
|
|
5393
|
+
}
|
|
4887
5394
|
}
|
|
4888
5395
|
}
|
|
4889
5396
|
else {
|
|
@@ -4951,6 +5458,19 @@ class BindingSession {
|
|
|
4951
5458
|
}
|
|
4952
5459
|
record.outerPatternPathInfo = null;
|
|
4953
5460
|
}
|
|
5461
|
+
// 3 段目以降(§1.11)。各段も互いに独立した資源なので 1 つずつ守る
|
|
5462
|
+
if (record.outerPatternPathInfosRest !== null) {
|
|
5463
|
+
const restPathInfos = record.outerPatternPathInfosRest;
|
|
5464
|
+
for (let i = 0; i < restPathInfos.length; i++) {
|
|
5465
|
+
try {
|
|
5466
|
+
removeBindingByPattern(restPathInfos[i], record.patternListIndex, binding);
|
|
5467
|
+
}
|
|
5468
|
+
catch {
|
|
5469
|
+
// Cleanup is best-effort.
|
|
5470
|
+
}
|
|
5471
|
+
}
|
|
5472
|
+
record.outerPatternPathInfosRest = null;
|
|
5473
|
+
}
|
|
4954
5474
|
try {
|
|
4955
5475
|
removeBindingByPattern(record.patternPathInfo, record.patternListIndex, binding);
|
|
4956
5476
|
record.patternPathInfo = null;
|
|
@@ -5129,7 +5649,8 @@ function applyChangeToCommand(binding, _context, newValue) {
|
|
|
5129
5649
|
raiseError(`command binding requires a wc-bindable custom element. <${element.tagName.toLowerCase()}> is not wc-bindable.`);
|
|
5130
5650
|
}
|
|
5131
5651
|
if (!bindable.declaredCommands.has(methodName)) {
|
|
5132
|
-
|
|
5652
|
+
// eventTokenHandler の property 検証と対双の did-you-mean(設計 §3)。
|
|
5653
|
+
raiseError(`Command "${methodName}" is not declared in wcBindable.commands of <${element.tagName.toLowerCase()}>.${didYouMean(methodName, bindable.declaredCommands.keys())}`);
|
|
5133
5654
|
}
|
|
5134
5655
|
// ここまで来たら旧解除して新 subscribe に切り替える。
|
|
5135
5656
|
if (existing) {
|
|
@@ -5789,7 +6310,7 @@ function compileRowPlan(fragmentInfo) {
|
|
|
5789
6310
|
// command.<name>(prop 扱い)と eventToken.<prop>(event 扱い)は token 配線の
|
|
5790
6311
|
// teardown / attach 分岐が要るため不適格
|
|
5791
6312
|
const namespace = template.propSegments[0];
|
|
5792
|
-
if (namespace ===
|
|
6313
|
+
if (namespace === COMMAND_NAMESPACE || namespace === EVENT_TOKEN_NAMESPACE) {
|
|
5793
6314
|
return null;
|
|
5794
6315
|
}
|
|
5795
6316
|
if (bindingType === "text") {
|
|
@@ -6883,12 +7404,15 @@ function scheduleDeferredApply(binding, tagName) {
|
|
|
6883
7404
|
}, reject);
|
|
6884
7405
|
}
|
|
6885
7406
|
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
7407
|
+
// キーは define.ts の namespace 語彙定数(manifest.syntax.bindingTypes.propNamespaces と
|
|
7408
|
+
// 同一の正本)。集合の一致は __tests__/manifest.test.ts の drift テストが強制するため
|
|
7409
|
+
// export する(manifest エントリは DOM 非依存でこのファイルを import できない)。
|
|
7410
|
+
const applyChangeByFirstSegment = Object.freeze({
|
|
7411
|
+
[CLASS_NAMESPACE]: applyChangeToClass,
|
|
7412
|
+
[ATTR_NAMESPACE]: applyChangeToAttribute,
|
|
7413
|
+
[STYLE_NAMESPACE]: applyChangeToStyle,
|
|
7414
|
+
[COMMAND_NAMESPACE]: applyChangeToCommand,
|
|
7415
|
+
});
|
|
6892
7416
|
const applyChangeByBindingType = {
|
|
6893
7417
|
"text": applyChangeToText,
|
|
6894
7418
|
"for": applyChangeToFor,
|
|
@@ -7453,9 +7977,17 @@ function _getFragmentInfo(rootNode, fragment, parseBindingTextResult, forPath) {
|
|
|
7453
7977
|
}
|
|
7454
7978
|
function collectStructuralFragments(rootNode, walkRoot, forPath) {
|
|
7455
7979
|
const elseKeyword = config.commentElsePrefix;
|
|
7980
|
+
// Light DOM の mapped コンポーネントの内側は、その子スコープが自分で処理する(§1.13)。
|
|
7981
|
+
// fragment info は rootNode + state 名で登録されるため、ホストのパスでここを拾うと
|
|
7982
|
+
// コンポーネント側の state がまだ名前登録を済ませておらず解決に失敗する。
|
|
7983
|
+
// コンポーネント要素自身は template ではないので、REJECT でサブツリーごと落として問題ない。
|
|
7984
|
+
const nestedComponents = findNestedLightDomComponents(walkRoot);
|
|
7456
7985
|
const walker = document.createTreeWalker(walkRoot, NodeFilter.SHOW_ELEMENT, {
|
|
7457
7986
|
acceptNode(node) {
|
|
7458
7987
|
const element = node;
|
|
7988
|
+
if (nestedComponents.length > 0 && nestedComponents.indexOf(element) !== -1) {
|
|
7989
|
+
return NodeFilter.FILTER_REJECT;
|
|
7990
|
+
}
|
|
7459
7991
|
if (element.tagName.toLowerCase() === 'template') {
|
|
7460
7992
|
const bindText = element.getAttribute(config.bindAttributeName) || '';
|
|
7461
7993
|
if (bindText.length > 0) {
|
|
@@ -7576,6 +8108,13 @@ async function waitForStateInitialize(root) {
|
|
|
7576
8108
|
const promises = [];
|
|
7577
8109
|
await customElements.whenDefined(config.tagNames.state);
|
|
7578
8110
|
for (const element of elements) {
|
|
8111
|
+
// Light DOM の mapped コンポーネントの state は待たない。それはこの root の
|
|
8112
|
+
// バインディングが張られてからでないと初期化できず(自分を束ねるホスト binding を
|
|
8113
|
+
// 待つ)、ここで待つと循環する(§1.13)。Shadow DOM 形では別 rootNode にいるので
|
|
8114
|
+
// そもそもこの集合に現れず、plain 形は循環しないので従来どおり待つ。
|
|
8115
|
+
if (isLightDomMappedStateElement(element)) {
|
|
8116
|
+
continue;
|
|
8117
|
+
}
|
|
7579
8118
|
const stateElement = element;
|
|
7580
8119
|
promises.push(stateElement.initializePromise);
|
|
7581
8120
|
}
|
|
@@ -7606,7 +8145,7 @@ async function buildBindings(root) {
|
|
|
7606
8145
|
}
|
|
7607
8146
|
}
|
|
7608
8147
|
|
|
7609
|
-
var version = "1.
|
|
8148
|
+
var version = "1.28.0";
|
|
7610
8149
|
var pkg = {
|
|
7611
8150
|
version: version};
|
|
7612
8151
|
|
|
@@ -8441,27 +8980,89 @@ function setStateElementByName(rootNode, name, element) {
|
|
|
8441
8980
|
}
|
|
8442
8981
|
}
|
|
8443
8982
|
|
|
8444
|
-
|
|
8983
|
+
/**
|
|
8984
|
+
* watch/chainDepth.ts
|
|
8985
|
+
*
|
|
8986
|
+
* `$watch` ハンドラ起点の書き込み連鎖の深さを数える台帳
|
|
8987
|
+
* (docs/state-watch-hook-design.md §7-2)。
|
|
8988
|
+
*
|
|
8989
|
+
* watch ハンドラ内の書き込みは新しい microtask バッチを作るため、伝播 context の
|
|
8990
|
+
* hop 上限(MAX_PROPAGATION_HOPS)のガードが効かない。かつ書き込み先が動的なので、
|
|
8991
|
+
* `$streams` のような「宣言時の自己依存検出」も使えない。よって実行時に数える。
|
|
8992
|
+
*
|
|
8993
|
+
* updater(enqueue 側)と watchRuntime(発火側)の両方から参照されるため、
|
|
8994
|
+
* **依存ゼロの葉モジュール**にして循環 import を避ける(devtools/sink.ts と同じ方針)。
|
|
8995
|
+
*
|
|
8996
|
+
* 数え方: ハンドラ実行中に enqueue が起きたときだけ「次のバッチはこの連鎖の続き」と
|
|
8997
|
+
* マークする。ハンドラが何も書かなければ次のバッチは深さ 0 に戻るので、利用者操作が
|
|
8998
|
+
* 何度続いても深さは伸びない。
|
|
8999
|
+
*/
|
|
9000
|
+
/** ハンドラ実行中に立つ「今の連鎖の深さ + 1」。0 なら watch 起点ではない */
|
|
9001
|
+
let firingDepth = 0;
|
|
9002
|
+
/** 次に drain されるバッチの深さ */
|
|
9003
|
+
let pendingDepth = 0;
|
|
9004
|
+
/** watch の発火フェーズ開始(watchRuntime 専用) */
|
|
9005
|
+
function beginWatchFiring(depth) {
|
|
9006
|
+
firingDepth = depth + 1;
|
|
9007
|
+
}
|
|
9008
|
+
/** watch の発火フェーズ終了(watchRuntime 専用。必ず finally で呼ぶ) */
|
|
9009
|
+
function endWatchFiring() {
|
|
9010
|
+
firingDepth = 0;
|
|
9011
|
+
}
|
|
9012
|
+
/**
|
|
9013
|
+
* 書き込みの enqueue を記録する(updater 専用)。
|
|
9014
|
+
* ハンドラ実行中でなければ何もしない = 通常の書き込みに深さは付かない。
|
|
9015
|
+
*/
|
|
9016
|
+
function noteEnqueueForWatchChain() {
|
|
9017
|
+
if (firingDepth > pendingDepth) {
|
|
9018
|
+
pendingDepth = firingDepth;
|
|
9019
|
+
}
|
|
9020
|
+
}
|
|
9021
|
+
/** 次バッチの深さを消費する(watchRuntime 専用。読んだらリセット) */
|
|
9022
|
+
function consumeWatchChainDepth() {
|
|
9023
|
+
const depth = pendingDepth;
|
|
9024
|
+
pendingDepth = 0;
|
|
9025
|
+
return depth;
|
|
9026
|
+
}
|
|
9027
|
+
|
|
9028
|
+
const updateBatchListeners = [];
|
|
8445
9029
|
/**
|
|
8446
9030
|
* drain 終了リスナーを登録する。
|
|
9031
|
+
*
|
|
9032
|
+
* `priority` の昇順に呼ばれる(同値は登録順)。機構間の実行順序
|
|
9033
|
+
* (`$watch` → `$streams` restart、docs/state-watch-hook-design.md §3-2 層 1)は
|
|
9034
|
+
* この優先度で固定する — import 順に順序を持たせると、無関係な import 整理で
|
|
9035
|
+
* 静かに壊れるため。定数は define.ts の `*_LISTENER_PRIORITY` を使うこと。
|
|
8447
9036
|
*/
|
|
8448
|
-
function registerUpdateBatchListener(listener) {
|
|
8449
|
-
|
|
9037
|
+
function registerUpdateBatchListener(listener, priority = 0) {
|
|
9038
|
+
// 挿入ソート: 同値優先度の中では登録順を保つ(find は最初の「より大きい」要素を指す)
|
|
9039
|
+
const index = updateBatchListeners.findIndex((registered) => registered.priority > priority);
|
|
9040
|
+
const entry = { listener, priority };
|
|
9041
|
+
if (index === -1) {
|
|
9042
|
+
updateBatchListeners.push(entry);
|
|
9043
|
+
}
|
|
9044
|
+
else {
|
|
9045
|
+
updateBatchListeners.splice(index, 0, entry);
|
|
9046
|
+
}
|
|
8450
9047
|
}
|
|
8451
9048
|
/**
|
|
8452
9049
|
* drain 終了リスナーを解除する(テスト間の分離用)。
|
|
8453
9050
|
*/
|
|
8454
9051
|
function unregisterUpdateBatchListener(listener) {
|
|
8455
|
-
updateBatchListeners.
|
|
9052
|
+
const index = updateBatchListeners.findIndex((registered) => registered.listener === listener);
|
|
9053
|
+
if (index !== -1) {
|
|
9054
|
+
updateBatchListeners.splice(index, 1);
|
|
9055
|
+
}
|
|
8456
9056
|
}
|
|
8457
9057
|
/**
|
|
8458
|
-
* 全リスナーに drain
|
|
9058
|
+
* 全リスナーに drain のバッチを優先度順で通知する。
|
|
8459
9059
|
* リスナーの throw は握りつぶさない(内部バグの隠蔽防止)。
|
|
8460
|
-
* stream 側リスナーが entry ごとに自前で try/catch する契約(設計書 §3-2)。
|
|
9060
|
+
* stream / watch 側リスナーが entry ごとに自前で try/catch する契約(設計書 §3-2)。
|
|
8461
9061
|
*/
|
|
8462
9062
|
function notifyUpdateBatchListeners(batch) {
|
|
8463
|
-
|
|
8464
|
-
|
|
9063
|
+
// 反復中の register / unregister(ハンドラ内の切断・再 set)に耐えるためコピーする
|
|
9064
|
+
for (const registered of updateBatchListeners.slice()) {
|
|
9065
|
+
registered.listener(batch);
|
|
8465
9066
|
}
|
|
8466
9067
|
}
|
|
8467
9068
|
class Updater {
|
|
@@ -8469,6 +9070,9 @@ class Updater {
|
|
|
8469
9070
|
constructor() {
|
|
8470
9071
|
}
|
|
8471
9072
|
enqueueAbsoluteAddress(absoluteAddress, context = null) {
|
|
9073
|
+
// `$watch` ハンドラ実行中の書き込みだけを連鎖としてマークする(watch/chainDepth.ts)。
|
|
9074
|
+
// ハンドラ実行中でなければ即 return する葉モジュール呼び出し 1 個のコスト。
|
|
9075
|
+
noteEnqueueForWatchChain();
|
|
8472
9076
|
const requireStartProcess = this._queueUpdateRecords.length === 0;
|
|
8473
9077
|
this._queueUpdateRecords.push({ absoluteAddress, context });
|
|
8474
9078
|
if (requireStartProcess) {
|
|
@@ -8695,7 +9299,9 @@ function setSink(sink) {
|
|
|
8695
9299
|
setDevtoolsSink(sink);
|
|
8696
9300
|
const isActive = sink !== null;
|
|
8697
9301
|
if (isActive && !wasActive) {
|
|
8698
|
-
|
|
9302
|
+
// `$watch` / `$streams` restart より先に流す(protocol §4.3)。優先度を省略しても
|
|
9303
|
+
// 既定 0 で結果は同じだが、それは偶然なので定数で意図を固定する。
|
|
9304
|
+
registerUpdateBatchListener(onUpdateBatch, DEVTOOLS_LISTENER_PRIORITY);
|
|
8699
9305
|
}
|
|
8700
9306
|
else if (!isActive && wasActive) {
|
|
8701
9307
|
unregisterUpdateBatchListener(onUpdateBatch);
|
|
@@ -9012,12 +9618,12 @@ function processCommandTokensDeclaration(state) {
|
|
|
9012
9618
|
return names;
|
|
9013
9619
|
}
|
|
9014
9620
|
|
|
9015
|
-
const registryByStateElement$
|
|
9621
|
+
const registryByStateElement$2 = new WeakMap();
|
|
9016
9622
|
function getOrCreateCommandToken(stateElement, name) {
|
|
9017
|
-
let registry = registryByStateElement$
|
|
9623
|
+
let registry = registryByStateElement$2.get(stateElement);
|
|
9018
9624
|
if (typeof registry === "undefined") {
|
|
9019
9625
|
registry = new Map();
|
|
9020
|
-
registryByStateElement$
|
|
9626
|
+
registryByStateElement$2.set(stateElement, registry);
|
|
9021
9627
|
}
|
|
9022
9628
|
let token = registry.get(name);
|
|
9023
9629
|
if (typeof token === "undefined") {
|
|
@@ -9027,7 +9633,7 @@ function getOrCreateCommandToken(stateElement, name) {
|
|
|
9027
9633
|
return token;
|
|
9028
9634
|
}
|
|
9029
9635
|
function clearCommandTokenRegistry(stateElement) {
|
|
9030
|
-
registryByStateElement$
|
|
9636
|
+
registryByStateElement$2.delete(stateElement);
|
|
9031
9637
|
}
|
|
9032
9638
|
|
|
9033
9639
|
/**
|
|
@@ -9139,7 +9745,7 @@ function processOnDeclaration(stateElement, state, eventTokenNames) {
|
|
|
9139
9745
|
}
|
|
9140
9746
|
for (const [name, handler] of Object.entries(declared)) {
|
|
9141
9747
|
if (!eventTokenNames.has(name)) {
|
|
9142
|
-
raiseError(`${STATE_ON_NAME} entry "${name}" is not declared in $eventTokens
|
|
9748
|
+
raiseError(`${STATE_ON_NAME} entry "${name}" is not declared in $eventTokens.${didYouMean(name, eventTokenNames)}`);
|
|
9143
9749
|
}
|
|
9144
9750
|
if (typeof handler !== "function") {
|
|
9145
9751
|
raiseError(`${STATE_ON_NAME} entry "${name}" must be a function.`);
|
|
@@ -9262,24 +9868,24 @@ function invalidateLastNotified(stateElement, name) {
|
|
|
9262
9868
|
* 設計書 §3-2 の「未接続(disconnect 済み)の stateElement の entry は restart
|
|
9263
9869
|
* しない」はこの不変条件で担保される。
|
|
9264
9870
|
*/
|
|
9265
|
-
const activeStateElements = new Set();
|
|
9871
|
+
const activeStateElements$1 = new Set();
|
|
9266
9872
|
/**
|
|
9267
9873
|
* 起動中 stateElement として登録する(startStreams 専用。不変条件はモジュールヘッダ参照)。
|
|
9268
9874
|
*/
|
|
9269
9875
|
function addActiveStateElement(stateElement) {
|
|
9270
|
-
activeStateElements.add(stateElement);
|
|
9876
|
+
activeStateElements$1.add(stateElement);
|
|
9271
9877
|
}
|
|
9272
9878
|
/**
|
|
9273
9879
|
* 起動中 stateElement から外す(abortAllStreams / clearStreamRegistry 専用)。
|
|
9274
9880
|
*/
|
|
9275
9881
|
function deleteActiveStateElement(stateElement) {
|
|
9276
|
-
activeStateElements.delete(stateElement);
|
|
9882
|
+
activeStateElements$1.delete(stateElement);
|
|
9277
9883
|
}
|
|
9278
9884
|
/**
|
|
9279
9885
|
* 起動中 stateElement を列挙する(drain リスナーの交差判定用)。
|
|
9280
9886
|
*/
|
|
9281
9887
|
function getActiveStateElements() {
|
|
9282
|
-
return activeStateElements;
|
|
9888
|
+
return activeStateElements$1;
|
|
9283
9889
|
}
|
|
9284
9890
|
|
|
9285
9891
|
/**
|
|
@@ -9292,18 +9898,18 @@ function getActiveStateElements() {
|
|
|
9292
9898
|
* - disconnect 時は abortAllStreams(abort のみ・registry 保持)、
|
|
9293
9899
|
* `_state` 再 set 時のみ clearStreamRegistry(abort + 全削除)。
|
|
9294
9900
|
*/
|
|
9295
|
-
const registryByStateElement = new WeakMap();
|
|
9901
|
+
const registryByStateElement$1 = new WeakMap();
|
|
9296
9902
|
/**
|
|
9297
9903
|
* stream entry 群を置換登録する(`_state` セッターからの再構築で丸ごと差し替える)。
|
|
9298
9904
|
*/
|
|
9299
9905
|
function setStreamEntries(stateElement, entries) {
|
|
9300
|
-
registryByStateElement.set(stateElement, entries);
|
|
9906
|
+
registryByStateElement$1.set(stateElement, entries);
|
|
9301
9907
|
}
|
|
9302
9908
|
/**
|
|
9303
9909
|
* 登録済みの stream entry 群を返す。未登録なら空 Map を返す(registry への登録はしない)。
|
|
9304
9910
|
*/
|
|
9305
9911
|
function getStreamEntries(stateElement) {
|
|
9306
|
-
return registryByStateElement.get(stateElement) ?? new Map();
|
|
9912
|
+
return registryByStateElement$1.get(stateElement) ?? new Map();
|
|
9307
9913
|
}
|
|
9308
9914
|
/**
|
|
9309
9915
|
* 全 stream を abort して idle に戻す(設計書 §5-1)。registry は保持する。
|
|
@@ -9323,7 +9929,7 @@ function abortAllStreams(stateElement) {
|
|
|
9323
9929
|
// 設計書 §3-2。add 側は startStreams — stream/activeStateElements.ts の
|
|
9324
9930
|
// リーク防止不変条件を参照)。registry の有無に関わらず必ず外す。
|
|
9325
9931
|
deleteActiveStateElement(stateElement);
|
|
9326
|
-
const entries = registryByStateElement.get(stateElement);
|
|
9932
|
+
const entries = registryByStateElement$1.get(stateElement);
|
|
9327
9933
|
if (typeof entries === "undefined") {
|
|
9328
9934
|
return;
|
|
9329
9935
|
}
|
|
@@ -9343,7 +9949,7 @@ function clearStreamRegistry(stateElement) {
|
|
|
9343
9949
|
// abortAllStreams が既に delete 済みだが、「clear = 全削除でも必ず restart 対象から
|
|
9344
9950
|
// 外れる」不変条件を将来の abortAllStreams の変更から独立に保証するため明示的に呼ぶ。
|
|
9345
9951
|
deleteActiveStateElement(stateElement);
|
|
9346
|
-
registryByStateElement.delete(stateElement);
|
|
9952
|
+
registryByStateElement$1.delete(stateElement);
|
|
9347
9953
|
}
|
|
9348
9954
|
|
|
9349
9955
|
/**
|
|
@@ -10069,7 +10675,503 @@ function restartStreamsOnUpdateBatch(batch) {
|
|
|
10069
10675
|
}
|
|
10070
10676
|
}
|
|
10071
10677
|
}
|
|
10072
|
-
|
|
10678
|
+
// 優先度で `$watch` の後に固定する(設計書 §3-2 層 1)。import 順には依存しない。
|
|
10679
|
+
registerUpdateBatchListener(restartStreamsOnUpdateBatch, STREAM_LISTENER_PRIORITY);
|
|
10680
|
+
|
|
10681
|
+
/**
|
|
10682
|
+
* watch/watchRegistry.ts
|
|
10683
|
+
*
|
|
10684
|
+
* `$watch` の registry と、drain リスナーの走査元になる「発火対象の stateElement 集合」
|
|
10685
|
+
* (docs/state-watch-hook-design.md §9)。
|
|
10686
|
+
*
|
|
10687
|
+
* `$streams` は registry(delete 側)と runtime(add 側)が相互に依存するため
|
|
10688
|
+
* active 集合を stream/activeStateElements.ts へ切り出しているが、`$watch` は
|
|
10689
|
+
* **add が State のライフサイクル側、delete が registry 側**で、runtime は読むだけの
|
|
10690
|
+
* 一方向依存になる。よって循環せず、1 モジュールにまとめられる。
|
|
10691
|
+
*
|
|
10692
|
+
* リーク防止の不変条件(strong Set が切断済み要素の GC を妨げないための連動):
|
|
10693
|
+
* - add は `startWatch`(`State.connectedCallback` の $connectedCallback 完了後、および
|
|
10694
|
+
* 接続中の `_state` 再 set)だけが行い、**宣言が 1 つも無い stateElement は入れない**。
|
|
10695
|
+
* - delete は `deactivateWatch`(disconnectedCallback)/`clearWatchRegistry`(`_state`
|
|
10696
|
+
* 再 set)だけが行う。
|
|
10697
|
+
* どちらの経路も必ずここを通るため「Set に居る = 接続中かつ宣言済み」が保たれる。
|
|
10698
|
+
* この「宣言済み」の側が崩れると、`$watch` 未使用アプリの drain にも収集ループが乗る
|
|
10699
|
+
* (ゼロコスト契約、docs/state-watch-hook-design.md §10)。
|
|
10700
|
+
*/
|
|
10701
|
+
const registryByStateElement = new WeakMap();
|
|
10702
|
+
const activeStateElements = new Set();
|
|
10703
|
+
/**
|
|
10704
|
+
* 未登録時に返す共有の空 Map。
|
|
10705
|
+
*
|
|
10706
|
+
* ここで毎回 `new Map()` すると、drain の収集ループが「バッチのアドレス 1 個につき
|
|
10707
|
+
* Map を 1 個」アロケートすることになる(発火対象だが宣言を持たない stateElement を
|
|
10708
|
+
* 通る経路)。読み出ししかしない返り値なので 1 個を使い回す。
|
|
10709
|
+
*/
|
|
10710
|
+
const EMPTY_ENTRIES = new Map();
|
|
10711
|
+
/**
|
|
10712
|
+
* watch entry 群を置換登録する(`_state` セッターからの再構築で丸ごと差し替える)。
|
|
10713
|
+
*/
|
|
10714
|
+
function setWatchEntries(stateElement, entries) {
|
|
10715
|
+
registryByStateElement.set(stateElement, entries);
|
|
10716
|
+
}
|
|
10717
|
+
/**
|
|
10718
|
+
* 登録済みの watch entry 群を返す。未登録なら共有の空 Map を返す
|
|
10719
|
+
* (registry への登録はしない。返り値は読み出し専用)。
|
|
10720
|
+
*/
|
|
10721
|
+
function getWatchEntries(stateElement) {
|
|
10722
|
+
return registryByStateElement.get(stateElement) ?? EMPTY_ENTRIES;
|
|
10723
|
+
}
|
|
10724
|
+
/**
|
|
10725
|
+
* 発火対象として登録する(`startWatch` 専用。不変条件はモジュールヘッダ参照)。
|
|
10726
|
+
*/
|
|
10727
|
+
function addActiveWatchStateElement(stateElement) {
|
|
10728
|
+
activeStateElements.add(stateElement);
|
|
10729
|
+
}
|
|
10730
|
+
/**
|
|
10731
|
+
* 発火対象を列挙する(drain リスナーの early return 判定用)。
|
|
10732
|
+
*/
|
|
10733
|
+
function getActiveWatchStateElements() {
|
|
10734
|
+
return activeStateElements;
|
|
10735
|
+
}
|
|
10736
|
+
/**
|
|
10737
|
+
* 発火対象から外す(切断時)。**registry は保持する。**
|
|
10738
|
+
*
|
|
10739
|
+
* `$streams` の abortAllStreams と同じ二段構えで、切断は「発火しなくなる」だけにする。
|
|
10740
|
+
* registry まで捨てると、再接続(connectedCallback → startWatch)で宣言を作り直す経路が
|
|
10741
|
+
* 無い(`_state` セッターは初回ロード時にしか走らない)ため、watch が二度と発火しない。
|
|
10742
|
+
*/
|
|
10743
|
+
function deactivateWatch(stateElement) {
|
|
10744
|
+
activeStateElements.delete(stateElement);
|
|
10745
|
+
}
|
|
10746
|
+
/**
|
|
10747
|
+
* registry から削除し、発火対象からも外す(`_state` 再 set 時の再配線用)。
|
|
10748
|
+
*/
|
|
10749
|
+
function clearWatchRegistry(stateElement) {
|
|
10750
|
+
activeStateElements.delete(stateElement);
|
|
10751
|
+
registryByStateElement.delete(stateElement);
|
|
10752
|
+
}
|
|
10753
|
+
|
|
10754
|
+
/**
|
|
10755
|
+
* watch/processWatchDeclaration.ts
|
|
10756
|
+
*
|
|
10757
|
+
* `$watch: { "<path>": (cur, prev, ...indexes) => void }` 宣言マップを解析し、
|
|
10758
|
+
* IWatchEntry を構築して watchRegistry に一括登録する
|
|
10759
|
+
* (docs/state-watch-hook-design.md §2-2 / §8)。
|
|
10760
|
+
*
|
|
10761
|
+
* `$streams` の processStreamsDeclaration と対称だが、**キーが宣言名ではなくパス**である
|
|
10762
|
+
* ぶん検証が異なる(`.` / `*` を許可し、代わりにパスとしての妥当性を見る)。
|
|
10763
|
+
*
|
|
10764
|
+
* 依存グラフ登録(§8)がこの関数の要点:
|
|
10765
|
+
* `setPathInfo` は BindingSession(= DOM バインディング登録)からしか呼ばれないため、
|
|
10766
|
+
* 静的依存グラフに載るのは「バインドされたパス」だけである。watch を宣言しただけでは
|
|
10767
|
+
* walkDependency がそのパスを知らず、`items` への代入で `items.*.price` がバッチに載らない
|
|
10768
|
+
* = ハンドラが黙って一度も発火しない。宣言時に自分で登録することでこれを塞ぐ。
|
|
10769
|
+
*
|
|
10770
|
+
* 呼び出しは stateElement の `_pathSet` クリア後・getterPaths 確定後であること
|
|
10771
|
+
* (State の `_state` セッターが順序を保証する)。
|
|
10772
|
+
*/
|
|
10773
|
+
/**
|
|
10774
|
+
* `$watch` 宣言を registry へ反映し、監視対象パスの集合を返す。
|
|
10775
|
+
*
|
|
10776
|
+
* 宣言が無い(または空)なら **null** を返す。呼び出し側(State)はこれを
|
|
10777
|
+
* `watchPaths` に保持し、setByAddress のホットパスは `!== null` の分岐 1 個で
|
|
10778
|
+
* 抜けられる(ゼロコスト契約、§10)。
|
|
10779
|
+
*/
|
|
10780
|
+
function processWatchDeclaration(stateElement, state) {
|
|
10781
|
+
const declared = state[STATE_WATCH_NAME];
|
|
10782
|
+
if (typeof declared === "undefined") {
|
|
10783
|
+
return null;
|
|
10784
|
+
}
|
|
10785
|
+
if (typeof declared !== "object" || declared === null) {
|
|
10786
|
+
// 非オブジェクト形は lint 側では候補ゼロ扱いで検出されないため LINT_HINT なし
|
|
10787
|
+
// (以下、lint が実際に検出する shape(非関数・$ 始まり・@ 越境・空セグメント)にだけ付ける)。
|
|
10788
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} must be an object mapping state paths to handler functions.`);
|
|
10789
|
+
}
|
|
10790
|
+
const entries = new Map();
|
|
10791
|
+
const paths = new Set();
|
|
10792
|
+
let order = 0;
|
|
10793
|
+
for (const [path, handler] of Object.entries(declared)) {
|
|
10794
|
+
if (typeof handler !== "function") {
|
|
10795
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" must be a function.${LINT_HINT}`);
|
|
10796
|
+
}
|
|
10797
|
+
if (path.length === 0) {
|
|
10798
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry name must be a non-empty state path.`);
|
|
10799
|
+
}
|
|
10800
|
+
if (path.startsWith("$")) {
|
|
10801
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" must not start with "$" (reserved namespace).${LINT_HINT}`);
|
|
10802
|
+
}
|
|
10803
|
+
// 越境 watch は不採用(設計 D8)。他 state のアドレスは発火対象にしないため、
|
|
10804
|
+
// `@stateName` 付きのパスは受け取った時点で落とす(黙って発火しないより良い)。
|
|
10805
|
+
if (path.includes(STATE_NAME_SEPARATOR)) {
|
|
10806
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" must not target another state ("${STATE_NAME_SEPARATOR}" is not allowed); watch only paths of its own state.${LINT_HINT}`);
|
|
10807
|
+
}
|
|
10808
|
+
// Object.prototype の継承名は `path in state` 系の判定を汚すため一律拒否する
|
|
10809
|
+
// (processStreamsDeclaration と同じ防衛線)。
|
|
10810
|
+
if (path in Object.prototype) {
|
|
10811
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" must not be a property name inherited from Object.prototype (e.g. "__proto__", "constructor").`);
|
|
10812
|
+
}
|
|
10813
|
+
const pathInfo = getPathInfo(path);
|
|
10814
|
+
// 空セグメント("a..b" / 先頭・末尾の ".")は getPathInfo が黙って受理してしまうため、
|
|
10815
|
+
// ここで落とす。放置すると解決不能なアドレスを依存グラフへ登録することになる。
|
|
10816
|
+
for (const segment of pathInfo.segments) {
|
|
10817
|
+
if (segment.length === 0) {
|
|
10818
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" has an empty path segment.${LINT_HINT}`);
|
|
10819
|
+
}
|
|
10820
|
+
}
|
|
10821
|
+
if (pathInfo.wildcardCount > MAX_WILDCARD_DEPTH) {
|
|
10822
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" exceeds the maximum wildcard depth (${MAX_WILDCARD_DEPTH}).`);
|
|
10823
|
+
}
|
|
10824
|
+
entries.set(path, {
|
|
10825
|
+
path,
|
|
10826
|
+
pathInfo,
|
|
10827
|
+
handler: handler,
|
|
10828
|
+
order: order++,
|
|
10829
|
+
});
|
|
10830
|
+
paths.add(path);
|
|
10831
|
+
// 依存グラフ登録(§8)。"for" 以外の bindingType は親 → 子の staticDependency
|
|
10832
|
+
// チェーンを生やすだけで listPaths / elementPaths を触らない(State.setPathInfo 参照)。
|
|
10833
|
+
stateElement.setPathInfo(path, "prop");
|
|
10834
|
+
}
|
|
10835
|
+
setWatchEntries(stateElement, entries);
|
|
10836
|
+
return paths.size > 0 ? paths : null;
|
|
10837
|
+
}
|
|
10838
|
+
|
|
10839
|
+
/**
|
|
10840
|
+
* watch/computedSnapshots.ts
|
|
10841
|
+
*
|
|
10842
|
+
* watch 対象の computed(getter)の「前回評価値」台帳
|
|
10843
|
+
* (docs/state-watch-hook-design.md §5)。
|
|
10844
|
+
*
|
|
10845
|
+
* getter は `setByAddress` を通らないので、スカラ書き込み用の旧値台帳
|
|
10846
|
+
* (watch/prevValues.ts)には載らない。`prev` を渡すには前回の評価値を
|
|
10847
|
+
* **バッチを跨いで**保持する必要があり、こちらは drain ごとにクリアしない。
|
|
10848
|
+
*
|
|
10849
|
+
* 寿命は stateElement 単位(WeakMap)。`_state` 再 set では宣言ごと作り直すため
|
|
10850
|
+
* 破棄する。切断では破棄しない —— 再接続時の初回評価が上書きするので、
|
|
10851
|
+
* 残っていても害がなく、registry を保持する扱いとも揃う。
|
|
10852
|
+
*/
|
|
10853
|
+
const snapshotsByStateElement = new WeakMap();
|
|
10854
|
+
function getComputedSnapshot(stateElement, absAddress) {
|
|
10855
|
+
return snapshotsByStateElement.get(stateElement)?.get(absAddress);
|
|
10856
|
+
}
|
|
10857
|
+
function setComputedSnapshot(stateElement, absAddress, value) {
|
|
10858
|
+
let snapshots = snapshotsByStateElement.get(stateElement);
|
|
10859
|
+
if (typeof snapshots === "undefined") {
|
|
10860
|
+
snapshots = new Map();
|
|
10861
|
+
snapshotsByStateElement.set(stateElement, snapshots);
|
|
10862
|
+
}
|
|
10863
|
+
snapshots.set(absAddress, value);
|
|
10864
|
+
}
|
|
10865
|
+
/** `_state` 再 set で宣言ごと作り直すときに破棄する */
|
|
10866
|
+
function clearComputedSnapshots(stateElement) {
|
|
10867
|
+
snapshotsByStateElement.delete(stateElement);
|
|
10868
|
+
}
|
|
10869
|
+
|
|
10870
|
+
/**
|
|
10871
|
+
* watch/prevValues.ts
|
|
10872
|
+
*
|
|
10873
|
+
* `$watch` ハンドラへ渡す `prev`(バッチ開始時点の値)の台帳
|
|
10874
|
+
* (docs/state-watch-hook-design.md §4-1)。
|
|
10875
|
+
*
|
|
10876
|
+
* 記録するのは **watch 宣言済みパスへの書き込みだけ**、かつ **バッチ内で最初の 1 回だけ**
|
|
10877
|
+
* (first-write-wins)。したがって `prev` は「そのバッチが始まる前の値」、`cur` は
|
|
10878
|
+
* drain 時点の確定値になる。同一バッチ内の中間値は観測できない(§3-4)。
|
|
10879
|
+
*
|
|
10880
|
+
* 値の出どころは same-value guard が既に読んでいる旧値であり、watch のために
|
|
10881
|
+
* 追加の getByAddress は行わない(§10)。その帰結として:
|
|
10882
|
+
* - 参照型(object / array)は guard が素通しするので `prev` は undefined
|
|
10883
|
+
* - `config.sameValueGuard = false` でも undefined
|
|
10884
|
+
* - `$postUpdate` / stream の status 通知は setByAddress を通らないので undefined
|
|
10885
|
+
*
|
|
10886
|
+
* 台帳は drain 終端(watchRuntime)でクリアされる。drain の外で書き込まれた分が
|
|
10887
|
+
* 次のバッチへ持ち越されることはない。
|
|
10888
|
+
*/
|
|
10889
|
+
const prevValueByAbsoluteStateAddress = new Map();
|
|
10890
|
+
/**
|
|
10891
|
+
* バッチ内で最初の書き込みのときだけ旧値を記録する(first-write-wins)。
|
|
10892
|
+
*/
|
|
10893
|
+
function recordPrevValue(absAddress, oldValue) {
|
|
10894
|
+
if (prevValueByAbsoluteStateAddress.has(absAddress)) {
|
|
10895
|
+
return;
|
|
10896
|
+
}
|
|
10897
|
+
prevValueByAbsoluteStateAddress.set(absAddress, oldValue);
|
|
10898
|
+
}
|
|
10899
|
+
/**
|
|
10900
|
+
* 記録済みの旧値を返す。記録が無ければ undefined(§4-1 の「prev を保証しない」経路)。
|
|
10901
|
+
*/
|
|
10902
|
+
function getPrevValue(absAddress) {
|
|
10903
|
+
return prevValueByAbsoluteStateAddress.get(absAddress);
|
|
10904
|
+
}
|
|
10905
|
+
/**
|
|
10906
|
+
* 台帳をクリアする(drain 終端で必ず呼ぶ)。
|
|
10907
|
+
*/
|
|
10908
|
+
function clearPrevValues() {
|
|
10909
|
+
prevValueByAbsoluteStateAddress.clear();
|
|
10910
|
+
}
|
|
10911
|
+
|
|
10912
|
+
/**
|
|
10913
|
+
* watch/watchRuntime.ts
|
|
10914
|
+
*
|
|
10915
|
+
* `$watch` の発火(docs/state-watch-hook-design.md §3 / §7)。
|
|
10916
|
+
*
|
|
10917
|
+
* updater の drain 終了フックに 1 つだけリスナーを登録し、バッチに載った絶対アドレスと
|
|
10918
|
+
* 宣言済み watch パスを突き合わせて発火する。**binding の有無に関係なくバッチへ載る**ので、
|
|
10919
|
+
* これが headless 購読の実体になる(binding 駆動の `$updatedCallback` との違い)。
|
|
10920
|
+
*
|
|
10921
|
+
* 実行順序(設計書 §3-2):
|
|
10922
|
+
* - 機構間は優先度で固定(`$updatedCallback` → `$watch` → `$streams` restart)。
|
|
10923
|
+
* `$updatedCallback` が先なのは binding 適用ループの内側で呼ばれる構造的必然。
|
|
10924
|
+
* - watch ハンドラ間は `$watch` の宣言順(entry.order)。利用者が順序に意思を持てる唯一の層。
|
|
10925
|
+
* - 同一パスの複数行は indexes 昇順。
|
|
10926
|
+
*
|
|
10927
|
+
* 収集と発火を 2 相に分ける理由:
|
|
10928
|
+
* 1. ハンドラ内の書き込みが registry / active 集合を同期的に変えうる(`_state` 再 set・切断)
|
|
10929
|
+
* ため、発火直前に live 再チェックが要る(`$streams` の restart hits と同型)。
|
|
10930
|
+
* 2. 上記の順序規約のために hits をソートする必要がある(バッチの反復順は enqueue 順)。
|
|
10931
|
+
*/
|
|
10932
|
+
/**
|
|
10933
|
+
* この stateElement の `$watch` を有効化する(`State.connectedCallback` /接続中の
|
|
10934
|
+
* `_state` 再 set から呼ばれる。無効化は `clearWatchRegistry`)。
|
|
10935
|
+
*
|
|
10936
|
+
* `addActiveWatchStateElement` の薄いラッパではなく、**State が runtime を import する
|
|
10937
|
+
* 経路をここに一本化する**意味がある: drain リスナーの登録はこのモジュールの
|
|
10938
|
+
* 初期化副作用なので、registry だけを import すると発火機構ごと落ちる。
|
|
10939
|
+
* `$streams` の `startStreams` と対称の位置づけ。
|
|
10940
|
+
*
|
|
10941
|
+
* **宣言が 1 つも無ければ active 集合に入れない**(`startStreams` の
|
|
10942
|
+
* `entries.size === 0` early return と同型)。ここを無条件にすると active 集合が
|
|
10943
|
+
* 「接続中の全 `<wcs-state>`」になり、`fireWatchOnUpdateBatch` の early return が
|
|
10944
|
+
* 実アプリで効かなくなる = `$watch` 未使用アプリの drain にも収集ループが乗る
|
|
10945
|
+
* (ゼロコスト契約、設計書 §10 / 実装計画 P16)。
|
|
10946
|
+
*/
|
|
10947
|
+
function startWatch(stateElement) {
|
|
10948
|
+
if (getWatchEntries(stateElement).size === 0) {
|
|
10949
|
+
return;
|
|
10950
|
+
}
|
|
10951
|
+
addActiveWatchStateElement(stateElement);
|
|
10952
|
+
primeComputedWatches(stateElement);
|
|
10953
|
+
}
|
|
10954
|
+
/**
|
|
10955
|
+
* watch 対象の computed(getter)を 1 回評価する(設計書 §5-2 の eager 化、C-3)。
|
|
10956
|
+
*
|
|
10957
|
+
* これが要るのは、getter の依存(dynamicDependency)が**評価時にしか張られない**ため。
|
|
10958
|
+
* 一度も評価されていない getter は依存グラフに載らず、依存の書き込みが walkDependency で
|
|
10959
|
+
* そのパスへ到達しないので、バッチにも載らず watch が永久に発火しない。ここで 1 回
|
|
10960
|
+
* 読むことで依存が張られ、同時に `prev` の初期スナップショットが埋まる。
|
|
10961
|
+
*
|
|
10962
|
+
* **これが「watch した getter は lazy でなくなる」の実体**であり、設計書 §5-2 で
|
|
10963
|
+
* 規範として明記している副作用(毎バッチ評価・例外の表面化・依存の再登録)の起点。
|
|
10964
|
+
*
|
|
10965
|
+
* ワイルドカードを含む getter パス(`items.*.tax` など)は対象外: 初回評価に行ごとの
|
|
10966
|
+
* indexes が要り、全行評価は宣言しただけでリスト全体を舐めることになる。この形は
|
|
10967
|
+
* 「DOM にバインドされていれば発火する」ままとし、§5-3 に制約として書く。
|
|
10968
|
+
*/
|
|
10969
|
+
function primeComputedWatches(stateElement) {
|
|
10970
|
+
// 宣言が 1 つ以上あることは startWatch が保証済み
|
|
10971
|
+
const targets = [];
|
|
10972
|
+
for (const entry of getWatchEntries(stateElement).values()) {
|
|
10973
|
+
if (isScalarComputed(stateElement, entry)) {
|
|
10974
|
+
targets.push(entry);
|
|
10975
|
+
}
|
|
10976
|
+
}
|
|
10977
|
+
if (targets.length === 0) {
|
|
10978
|
+
// getter を watch していないなら createState ごと省く(宣言の大半はこちら)
|
|
10979
|
+
return;
|
|
10980
|
+
}
|
|
10981
|
+
stateElement.createState("readonly", (state) => {
|
|
10982
|
+
for (const entry of targets) {
|
|
10983
|
+
try {
|
|
10984
|
+
setComputedSnapshot(stateElement, absoluteAddressOf(stateElement, entry), state[entry.path]);
|
|
10985
|
+
}
|
|
10986
|
+
catch (e) {
|
|
10987
|
+
// 初回評価の throw は接続を巻き添えにしない(発火時と同じ隔離方針、§7-1)
|
|
10988
|
+
reportWatchError(stateElement, entry.path, "prime", e);
|
|
10989
|
+
}
|
|
10990
|
+
}
|
|
10991
|
+
});
|
|
10992
|
+
}
|
|
10993
|
+
/** ワイルドカードを含まない watch パスの絶対アドレス(listIndex は常に null) */
|
|
10994
|
+
function absoluteAddressOf(stateElement, entry) {
|
|
10995
|
+
return createAbsoluteStateAddress(getAbsolutePathInfo(stateElement, entry.pathInfo), null);
|
|
10996
|
+
}
|
|
10997
|
+
/**
|
|
10998
|
+
* 前回評価値のスナップショット台帳(computedSnapshots)に載せる entry か。
|
|
10999
|
+
*
|
|
11000
|
+
* ワイルドカードを含む getter を**除く**のが要点。除かないと台帳のキーが行ごとの
|
|
11001
|
+
* 絶対アドレス(= listIndex を強参照)になり、prune 経路が `_state` 再 set しか
|
|
11002
|
+
* 無いため、行が入れ替わり続けるページで単調増加する(リスト置換 5 回で 2→10 件を実測)。
|
|
11003
|
+
* そもそもワイルドカード getter は eager 化の対象外(設計書 §5-3)なので、
|
|
11004
|
+
* 「初回評価もしない・前回値も持たない」で primeComputedWatches と対称になる。
|
|
11005
|
+
* この形の `prev` は常に undefined(getter は setByAddress を通らない)。
|
|
11006
|
+
*/
|
|
11007
|
+
function isScalarComputed(stateElement, entry) {
|
|
11008
|
+
return entry.pathInfo.wildcardCount === 0 && stateElement.getterPaths.has(entry.path);
|
|
11009
|
+
}
|
|
11010
|
+
/**
|
|
11011
|
+
* throw を報告する(設計書 §7-1)。
|
|
11012
|
+
*
|
|
11013
|
+
* `console.error` だけだと **devtools からは「静かに握られた失敗」が見えない**。
|
|
11014
|
+
* watch は drain フックを `$streams` と共有しており、例外を watch 側で閉じるのが
|
|
11015
|
+
* 前提なので、閉じた事実をここで観測可能にしておく必要がある。
|
|
11016
|
+
* イベント生成は必ず `devtoolsSink !== null` の内側で行う(sink のコスト規範)。
|
|
11017
|
+
*/
|
|
11018
|
+
const WATCH_ERROR_SUBJECT = {
|
|
11019
|
+
prime: "initial evaluation of",
|
|
11020
|
+
evaluate: "evaluation of",
|
|
11021
|
+
handler: "handler for",
|
|
11022
|
+
};
|
|
11023
|
+
function reportWatchError(stateElement, path, phase, error) {
|
|
11024
|
+
console.error(`[@wcstack/state] $watch ${WATCH_ERROR_SUBJECT[phase]} "${path}" threw.`, error);
|
|
11025
|
+
if (devtoolsSink !== null) {
|
|
11026
|
+
devtoolsSink({
|
|
11027
|
+
type: "state:watch-error",
|
|
11028
|
+
phase,
|
|
11029
|
+
stateName: stateElement.name,
|
|
11030
|
+
path,
|
|
11031
|
+
error,
|
|
11032
|
+
});
|
|
11033
|
+
}
|
|
11034
|
+
}
|
|
11035
|
+
function fireWatchOnUpdateBatch(batch) {
|
|
11036
|
+
const activeStateElements = getActiveWatchStateElements();
|
|
11037
|
+
try {
|
|
11038
|
+
if (activeStateElements.size === 0) {
|
|
11039
|
+
// watch 未使用アプリの drain に配列・イテレータ割り当てのコストを載せない。
|
|
11040
|
+
// ここも finally を通す: 宣言済みの state が切断されている間(active からは
|
|
11041
|
+
// 外れるが watchPaths は残る)の書き込みで台帳に旧値が積まれるため、
|
|
11042
|
+
// クリアを早期 return の外に置くと次のバッチどころか永久に残る。
|
|
11043
|
+
return;
|
|
11044
|
+
}
|
|
11045
|
+
const depth = consumeWatchChainDepth();
|
|
11046
|
+
if (depth > MAX_WATCH_CHAIN_DEPTH) {
|
|
11047
|
+
// 打ち切るのは watch の発火のみ。値と binding 適用は巻き戻さない
|
|
11048
|
+
// (伝播 hop 上限超過時の quarantine と同じ姿勢、§7-2)。
|
|
11049
|
+
const paths = Array.from(batch, (absAddress) => absAddress.absolutePathInfo.pathInfo.path);
|
|
11050
|
+
console.error(`[@wcstack/state] $watch chain depth limit exceeded; watch handlers for this batch were skipped.`, { maxDepth: MAX_WATCH_CHAIN_DEPTH, paths });
|
|
11051
|
+
if (devtoolsSink !== null) {
|
|
11052
|
+
devtoolsSink({ type: "state:watch-chain-limit", maxDepth: MAX_WATCH_CHAIN_DEPTH, paths });
|
|
11053
|
+
}
|
|
11054
|
+
return;
|
|
11055
|
+
}
|
|
11056
|
+
// --- 収集フェーズ ---
|
|
11057
|
+
const hits = [];
|
|
11058
|
+
for (const absAddress of batch) {
|
|
11059
|
+
// stateName 文字列ではなく stateElement 参照で引く。AbsolutePathInfo は
|
|
11060
|
+
// stateElement 単位でキャッシュされるので、同名 state が複数の rootNode に
|
|
11061
|
+
// 居ても取り違えない(address/AbsolutePathInfo.ts)。他 state のアドレスは
|
|
11062
|
+
// ここで自然に落ちる = 越境しない(設計 D8)。
|
|
11063
|
+
const stateElement = absAddress.absolutePathInfo.stateElement;
|
|
11064
|
+
if (!activeStateElements.has(stateElement)) {
|
|
11065
|
+
continue;
|
|
11066
|
+
}
|
|
11067
|
+
const entry = getWatchEntries(stateElement).get(absAddress.absolutePathInfo.pathInfo.path);
|
|
11068
|
+
if (typeof entry === "undefined") {
|
|
11069
|
+
continue;
|
|
11070
|
+
}
|
|
11071
|
+
let indexes = [];
|
|
11072
|
+
if (entry.pathInfo.wildcardCount > 0) {
|
|
11073
|
+
if (absAddress.listIndex === null) {
|
|
11074
|
+
// ワイルドカードパスなのに行が特定できないヒット(リストの依存展開で載る
|
|
11075
|
+
// 中間アドレス等)。indexes を空のまま発火すると cur の解決($resolve)が
|
|
11076
|
+
// 「indexes 不足」で throw し、例外隔離に落ちて console.error だけが残る。
|
|
11077
|
+
// 行が定まらない以上ハンドラに渡せる意味が無いので、収集段階で落とす。
|
|
11078
|
+
continue;
|
|
11079
|
+
}
|
|
11080
|
+
indexes = getScopedIndexes(absAddress.listIndex, entry.pathInfo.wildcardCount);
|
|
11081
|
+
}
|
|
11082
|
+
hits.push({ stateElement, entry, absAddress, indexes });
|
|
11083
|
+
}
|
|
11084
|
+
if (hits.length === 0) {
|
|
11085
|
+
return;
|
|
11086
|
+
}
|
|
11087
|
+
hits.sort(compareHits);
|
|
11088
|
+
// --- 発火フェーズ ---
|
|
11089
|
+
beginWatchFiring(depth);
|
|
11090
|
+
try {
|
|
11091
|
+
for (const hit of hits) {
|
|
11092
|
+
// 先行ハンドラが同期的に切断や `_state` 再 set を行い得るため、発火直前に
|
|
11093
|
+
// 「まだ active か」「entry が現行 registry のものか」を再確認する。
|
|
11094
|
+
if (!activeStateElements.has(hit.stateElement) ||
|
|
11095
|
+
getWatchEntries(hit.stateElement).get(hit.entry.path) !== hit.entry) {
|
|
11096
|
+
continue;
|
|
11097
|
+
}
|
|
11098
|
+
fireOne(hit);
|
|
11099
|
+
}
|
|
11100
|
+
}
|
|
11101
|
+
finally {
|
|
11102
|
+
endWatchFiring();
|
|
11103
|
+
}
|
|
11104
|
+
}
|
|
11105
|
+
finally {
|
|
11106
|
+
// 旧値台帳はこの drain 限りのもの。次のバッチへ持ち越さない(§4-1)。
|
|
11107
|
+
clearPrevValues();
|
|
11108
|
+
}
|
|
11109
|
+
}
|
|
11110
|
+
/**
|
|
11111
|
+
* 層 2(宣言順)→ 層 3(indexes 昇順)の順に比較する(設計書 §3-3)。
|
|
11112
|
+
*/
|
|
11113
|
+
function compareHits(a, b) {
|
|
11114
|
+
if (a.entry.order !== b.entry.order) {
|
|
11115
|
+
return a.entry.order - b.entry.order;
|
|
11116
|
+
}
|
|
11117
|
+
const length = Math.min(a.indexes.length, b.indexes.length);
|
|
11118
|
+
for (let i = 0; i < length; i++) {
|
|
11119
|
+
if (a.indexes[i] !== b.indexes[i]) {
|
|
11120
|
+
return a.indexes[i] - b.indexes[i];
|
|
11121
|
+
}
|
|
11122
|
+
}
|
|
11123
|
+
return a.indexes.length - b.indexes.length;
|
|
11124
|
+
}
|
|
11125
|
+
/**
|
|
11126
|
+
* ハンドラ 1 つを発火する。**例外はここで閉じる**(設計書 §7-1)。
|
|
11127
|
+
*
|
|
11128
|
+
* drain リスナーの throw は握りつぶさない契約(updater.ts)なので、watch 側で捕まえないと
|
|
11129
|
+
* 1 つのユーザー例外が他の watch と `$streams` の restart を巻き添えにする。
|
|
11130
|
+
* `$connectedCallback` / `$updatedCallback` の loud fail とは意図的に異なる扱い。
|
|
11131
|
+
*
|
|
11132
|
+
* 報告は throw 元で分ける: `cur` の解決(watch した getter の強制評価 = §5-2 の副作用 b)と
|
|
11133
|
+
* ハンドラ本体では原因も直し方も違うため、同じ文言に丸めない。
|
|
11134
|
+
*/
|
|
11135
|
+
function fireOne(hit) {
|
|
11136
|
+
const { stateElement, entry, absAddress, indexes } = hit;
|
|
11137
|
+
// スカラ getter は setByAddress を通らないので旧値台帳に載らない。前回評価値の
|
|
11138
|
+
// スナップショット(バッチを跨いで生きる別台帳)から prev を取る(§5-2)。
|
|
11139
|
+
const isComputed = isScalarComputed(stateElement, entry);
|
|
11140
|
+
try {
|
|
11141
|
+
stateElement.createState("writable", (state) => {
|
|
11142
|
+
let cur;
|
|
11143
|
+
try {
|
|
11144
|
+
// 強制評価はここ。dirty なら再計算され、その結果が cur になる
|
|
11145
|
+
cur = readCurrentValue(state, entry, indexes);
|
|
11146
|
+
}
|
|
11147
|
+
catch (e) {
|
|
11148
|
+
// cur が得られない以上ハンドラは呼べない。次の hit へ進む
|
|
11149
|
+
reportWatchError(stateElement, entry.path, "evaluate", e);
|
|
11150
|
+
return;
|
|
11151
|
+
}
|
|
11152
|
+
const prev = isComputed ? getComputedSnapshot(stateElement, absAddress) : getPrevValue(absAddress);
|
|
11153
|
+
if (isComputed) {
|
|
11154
|
+
// ハンドラ本体が throw しても次回の prev は「今回の評価値」であるべきなので、
|
|
11155
|
+
// handler 呼び出しより前に更新する
|
|
11156
|
+
setComputedSnapshot(stateElement, absAddress, cur);
|
|
11157
|
+
}
|
|
11158
|
+
entry.handler.call(state, cur, prev, ...indexes);
|
|
11159
|
+
});
|
|
11160
|
+
}
|
|
11161
|
+
catch (e) {
|
|
11162
|
+
reportWatchError(stateElement, entry.path, "handler", e);
|
|
11163
|
+
}
|
|
11164
|
+
}
|
|
11165
|
+
function readCurrentValue(state, entry, indexes) {
|
|
11166
|
+
if (entry.pathInfo.wildcardCount === 0) {
|
|
11167
|
+
return state[entry.path];
|
|
11168
|
+
}
|
|
11169
|
+
// ワイルドカードを含むパスは素の読みでは解決できない。getScopedIndexes が返した列は
|
|
11170
|
+
// そのまま $resolve の引数として使える(list/wildcardLevel.ts の往復契約)。
|
|
11171
|
+
return state.$resolve(entry.path, indexes);
|
|
11172
|
+
}
|
|
11173
|
+
// 優先度で `$streams` の restart より先に固定する(設計書 §3-2 層 1)。import 順には依存しない。
|
|
11174
|
+
registerUpdateBatchListener(fireWatchOnUpdateBatch, WATCH_LISTENER_PRIORITY);
|
|
10073
11175
|
|
|
10074
11176
|
function getterFn(name) {
|
|
10075
11177
|
return function () {
|
|
@@ -10180,6 +11282,20 @@ function getAllPropertyDescriptors(obj) {
|
|
|
10180
11282
|
* 双方向バインド・spread・initialSync の bindable 判定が**警告なしで**丸ごと死ぬ。
|
|
10181
11283
|
* 自前のファクトリが自前の reader に棄却される状態なので、生成前に落とす。
|
|
10182
11284
|
*/
|
|
11285
|
+
/**
|
|
11286
|
+
* did-you-mean の候補(エラーパス専用)。`$` 予約名と継承 `constructor` を除き、
|
|
11287
|
+
* `$bindables` には値プロパティだけ・`$commands` にはメソッドだけを提案する
|
|
11288
|
+
* (逆側を提案すると次は「is a method / is not a method」エラーに嵌まるため)。
|
|
11289
|
+
*/
|
|
11290
|
+
function dccCandidateNames(descriptors, kind) {
|
|
11291
|
+
return Object.keys(descriptors).filter((key) => {
|
|
11292
|
+
if (key.startsWith("$") || key === "constructor") {
|
|
11293
|
+
return false;
|
|
11294
|
+
}
|
|
11295
|
+
const isMethod = typeof descriptors[key].value === "function";
|
|
11296
|
+
return kind === "method" ? isMethod : !isMethod;
|
|
11297
|
+
});
|
|
11298
|
+
}
|
|
10183
11299
|
function readNameList(state, declarationName) {
|
|
10184
11300
|
const declared = state[declarationName];
|
|
10185
11301
|
if (typeof declared === "undefined") {
|
|
@@ -10232,7 +11348,7 @@ function processDccDeclarations(state) {
|
|
|
10232
11348
|
streamBackedBindables.push(name);
|
|
10233
11349
|
continue;
|
|
10234
11350
|
}
|
|
10235
|
-
raiseError(`${STATE_BINDABLES_NAME} entry "${name}" is not declared on the state
|
|
11351
|
+
raiseError(`${STATE_BINDABLES_NAME} entry "${name}" is not declared on the state.${didYouMean(name, dccCandidateNames(descriptors, "value"))}`);
|
|
10236
11352
|
}
|
|
10237
11353
|
if (typeof descriptor.value === "function") {
|
|
10238
11354
|
raiseError(`${STATE_BINDABLES_NAME} entry "${name}" is a method. Declare it in ${STATE_COMMANDS_NAME} instead.`);
|
|
@@ -10241,7 +11357,7 @@ function processDccDeclarations(state) {
|
|
|
10241
11357
|
for (const name of commands) {
|
|
10242
11358
|
const descriptor = descriptors[name];
|
|
10243
11359
|
if (typeof descriptor === "undefined") {
|
|
10244
|
-
raiseError(`${STATE_COMMANDS_NAME} entry "${name}" is not declared on the state
|
|
11360
|
+
raiseError(`${STATE_COMMANDS_NAME} entry "${name}" is not declared on the state.${didYouMean(name, dccCandidateNames(descriptors, "method"))}`);
|
|
10245
11361
|
}
|
|
10246
11362
|
if (typeof descriptor.value !== "function") {
|
|
10247
11363
|
raiseError(`${STATE_COMMANDS_NAME} entry "${name}" is not a method. Declare it in ${STATE_BINDABLES_NAME} instead.`);
|
|
@@ -11270,13 +12386,22 @@ function selectExpansionIndexes(context, sourcePath, _lastValue, _newValue, list
|
|
|
11270
12386
|
const EMPTY_PATH_INFOS = [];
|
|
11271
12387
|
/**
|
|
11272
12388
|
* 位置だけが変わった行(movedRows)で展開すべきパス群を求める。
|
|
11273
|
-
* `${listPath}.*`
|
|
12389
|
+
* `${listPath}.*` 配下にある、$1 等を読んだ実績のある getter
|
|
11274
12390
|
* (indexDependentGetterPaths)だけを返す。行の同一性・listIndex は保たれ
|
|
11275
12391
|
* index 以外の入力が不変なので、index を読まない getter / 値パスは再評価不要。
|
|
11276
12392
|
* 戻り値:
|
|
11277
12393
|
* - IPathInfo[](空可): この各パスだけを行の listIndex で展開する
|
|
11278
12394
|
* - null: ネストしたワイルドカード配下に index 依存 getter がある
|
|
11279
12395
|
* (listIndex の階数が合わず個別展開できない)→ 呼び出し側で行全体展開に倒す
|
|
12396
|
+
*
|
|
12397
|
+
* 配下判定は staticMap の subtree 走査ではなく indexDependentGetterPaths 側の
|
|
12398
|
+
* プレフィックス照合で行う。静的依存グラフは `State.setPathInfo` が
|
|
12399
|
+
* 「バインドされたパスから親方向へ」張るため、DOM にバインドされていない中間
|
|
12400
|
+
* getter(`.label` だけを描画し `.rank` は `.label` からしか読まれない綴り)は
|
|
12401
|
+
* subtree に現れない。そこを走査すると index 依存 getter を取りこぼし、
|
|
12402
|
+
* 「index を読む getter が subtree に無い=位置のみ変わった行の値は不変」という
|
|
12403
|
+
* 呼び出し側の判断が偽になって、移動行の getter が古い値のまま残る。
|
|
12404
|
+
* この集合は $1 を読んだ getter の数しか持たないので、走査コストも subtree より小さい。
|
|
11280
12405
|
*/
|
|
11281
12406
|
function getMovedRowExpansionPaths(context, wildcardPath, depPathInfo) {
|
|
11282
12407
|
const indexGetters = context.stateElement.indexDependentGetterPaths;
|
|
@@ -11284,26 +12409,16 @@ function getMovedRowExpansionPaths(context, wildcardPath, depPathInfo) {
|
|
|
11284
12409
|
return EMPTY_PATH_INFOS;
|
|
11285
12410
|
}
|
|
11286
12411
|
let result = null;
|
|
11287
|
-
const
|
|
11288
|
-
const
|
|
11289
|
-
|
|
11290
|
-
|
|
11291
|
-
if (indexGetters.has(path)) {
|
|
11292
|
-
const pathInfo = getPathInfo(path);
|
|
11293
|
-
if (pathInfo.wildcardCount !== depPathInfo.wildcardCount) {
|
|
11294
|
-
return null;
|
|
11295
|
-
}
|
|
11296
|
-
(result ??= []).push(pathInfo);
|
|
12412
|
+
const prefix = wildcardPath + DELIMITER;
|
|
12413
|
+
for (const path of indexGetters) {
|
|
12414
|
+
if (path !== wildcardPath && !path.startsWith(prefix)) {
|
|
12415
|
+
continue;
|
|
11297
12416
|
}
|
|
11298
|
-
const
|
|
11299
|
-
if (
|
|
11300
|
-
|
|
11301
|
-
if (!seen.has(child)) {
|
|
11302
|
-
seen.add(child);
|
|
11303
|
-
queue.push(child);
|
|
11304
|
-
}
|
|
11305
|
-
}
|
|
12417
|
+
const pathInfo = getPathInfo(path);
|
|
12418
|
+
if (pathInfo.wildcardCount !== depPathInfo.wildcardCount) {
|
|
12419
|
+
return null;
|
|
11306
12420
|
}
|
|
12421
|
+
(result ??= []).push(pathInfo);
|
|
11307
12422
|
}
|
|
11308
12423
|
return result ?? EMPTY_PATH_INFOS;
|
|
11309
12424
|
}
|
|
@@ -11531,6 +12646,21 @@ function walkDependency(stateName, stateElement, startAddress, staticDependency,
|
|
|
11531
12646
|
* - finallyで必ず更新情報を登録し、再描画や依存解決に利用
|
|
11532
12647
|
* - getter/setter経由のスコープ切り替えも考慮した設計
|
|
11533
12648
|
*/
|
|
12649
|
+
/**
|
|
12650
|
+
* `$watch` の `prev` 台帳へ旧値を記録する(docs/state-watch-hook-design.md §4-1)。
|
|
12651
|
+
*
|
|
12652
|
+
* same-value guard が既に読んだ旧値だけを使い、watch のための追加読みはしない。
|
|
12653
|
+
* `$watch` 未宣言時のコストは `watchPaths` の null 判定 1 個に収める(§10)。
|
|
12654
|
+
*/
|
|
12655
|
+
function recordWatchPrevValue(stateElement, path, absAddress, oldValue, hasOldValue) {
|
|
12656
|
+
const watchPaths = stateElement.watchPaths;
|
|
12657
|
+
if (watchPaths == null || !hasOldValue) {
|
|
12658
|
+
return;
|
|
12659
|
+
}
|
|
12660
|
+
if (watchPaths.has(path)) {
|
|
12661
|
+
recordPrevValue(absAddress, oldValue);
|
|
12662
|
+
}
|
|
12663
|
+
}
|
|
11534
12664
|
// Phase 3: 書き込み時点の因果 context を update record に付与する。
|
|
11535
12665
|
// binding 経由の書き込みは呼び出し元の dynamic scope から context を引き継ぎ、
|
|
11536
12666
|
// binding 外からの API update は新しい transaction を開始する(設計書 §4 規則 1)。
|
|
@@ -11764,6 +12894,7 @@ function setByAddressCore(target, address, value, receiver, handler, keyedMergeP
|
|
|
11764
12894
|
hasOldValue: devHasOldValue,
|
|
11765
12895
|
});
|
|
11766
12896
|
}
|
|
12897
|
+
recordWatchPrevValue(stateElement, path, absAddress, devOldValue, devHasOldValue);
|
|
11767
12898
|
try {
|
|
11768
12899
|
if (key === undefined) {
|
|
11769
12900
|
raiseError(`address.listIndex?.index is undefined path: ${path}`);
|
|
@@ -11814,6 +12945,7 @@ function setByAddressCore(target, address, value, receiver, handler, keyedMergeP
|
|
|
11814
12945
|
hasOldValue: devHasOldValue,
|
|
11815
12946
|
});
|
|
11816
12947
|
}
|
|
12948
|
+
recordWatchPrevValue(stateElement, path, absAddress, devOldValue, devHasOldValue);
|
|
11817
12949
|
try {
|
|
11818
12950
|
if (isSwappable) {
|
|
11819
12951
|
return _setByAddressWithSwap(target, address, absAddress, value, receiver, handler, keyedMergePath);
|
|
@@ -12558,14 +13690,19 @@ class InnerStateProxyHandler {
|
|
|
12558
13690
|
*/
|
|
12559
13691
|
_outerLoopContext(innerPathInfo, outerAbsPathInfo) {
|
|
12560
13692
|
const outerWildcardCount = outerAbsPathInfo.pathInfo.wildcardCount;
|
|
13693
|
+
// 段数の照合は外側スコープの**実 arity**(パスの段数 + そのスコープの Δ)で行う。
|
|
13694
|
+
// 境界 1 枚なら外側は Δ=0 で従来と同値、2 枚以上あるときに中間スコープの Δ を
|
|
13695
|
+
// 数えないと候補が両方とも外れて loopContext が null になる(§1.12)。
|
|
13696
|
+
// 添字(wildcardPaths)に使うのは Δ を含まない段数のままであることに注意。
|
|
13697
|
+
const outerArity = getScopeArity(outerAbsPathInfo.stateElement, outerAbsPathInfo.pathInfo);
|
|
12561
13698
|
const nodeLoopContext = getLoopContextByNode(this._webComponent);
|
|
12562
|
-
if (nodeLoopContext !== null && nodeLoopContext.listIndex.length ===
|
|
13699
|
+
if (nodeLoopContext !== null && nodeLoopContext.listIndex.length === outerArity) {
|
|
12563
13700
|
return nodeLoopContext;
|
|
12564
13701
|
}
|
|
12565
13702
|
if (outerWildcardCount > 0) {
|
|
12566
13703
|
const address = getCrossBoundaryAddress(this._innerStateElement, innerPathInfo.path);
|
|
12567
13704
|
const listIndex = address?.listIndex ?? null;
|
|
12568
|
-
if (listIndex !== null && listIndex.length ===
|
|
13705
|
+
if (listIndex !== null && listIndex.length === outerArity) {
|
|
12569
13706
|
const outerWildcardPath = outerAbsPathInfo.pathInfo.wildcardPaths[outerWildcardCount - 1];
|
|
12570
13707
|
return createStateAddress(getPathInfo(outerWildcardPath), listIndex);
|
|
12571
13708
|
}
|
|
@@ -12837,6 +13974,8 @@ class State extends HTMLElementBase {
|
|
|
12837
13974
|
_dynamicDependency = new Map();
|
|
12838
13975
|
_staticDependency = new Map();
|
|
12839
13976
|
_pathSet = new Set();
|
|
13977
|
+
// `$watch` 宣言の監視対象パス。宣言が無ければ null(setByAddress のゼロコスト契約)
|
|
13978
|
+
_watchPaths = null;
|
|
12840
13979
|
_version = 0;
|
|
12841
13980
|
_rootNode = null;
|
|
12842
13981
|
_boundComponent = null;
|
|
@@ -12916,10 +14055,21 @@ class State extends HTMLElementBase {
|
|
|
12916
14055
|
// $listKeys: 宣言が無ければ null のままで、setByAddress のキー突合経路には
|
|
12917
14056
|
// 一切入らない(docs/state-list-key-design.md §7-1)。再 set で必ず置き換える。
|
|
12918
14057
|
this._listKeys = processListKeysDeclaration(value);
|
|
14058
|
+
// $watch: 旧宣言のハンドラが残らないよう registry を落としてから新宣言を解析する。
|
|
14059
|
+
// _pathSet.clear() の後であること(依存グラフ登録をやり直す必要がある、
|
|
14060
|
+
// docs/state-watch-hook-design.md §8)。宣言が無ければ watchPaths は null で、
|
|
14061
|
+
// setByAddress の旧値キャプチャには一切入らない(§10 のゼロコスト契約)。
|
|
14062
|
+
clearWatchRegistry(this);
|
|
14063
|
+
// computed の前回評価値も宣言と寿命を共にする(旧宣言の値を新しい watch の
|
|
14064
|
+
// prev として渡さない)。切断では消さない — 再接続の初回評価が上書きする。
|
|
14065
|
+
clearComputedSnapshots(this);
|
|
14066
|
+
this._watchPaths = processWatchDeclaration(this, value);
|
|
12919
14067
|
// 接続中の再 set(S13)は新宣言で即再起動する。
|
|
12920
14068
|
// 初回(_initialize 中)は _initialized が false なのでここでは起動されず、
|
|
12921
14069
|
// connectedCallback 側の startStreams($connectedCallback 完了後)が担う。
|
|
12922
14070
|
if (this._initialized && this._rootNode !== null && !inSsr()) {
|
|
14071
|
+
// watch は stream より先に有効化する(stream の起動時書き込みを観測できるように)
|
|
14072
|
+
startWatch(this);
|
|
12923
14073
|
startStreams(this);
|
|
12924
14074
|
// $connectedCallback 実行中の再 set(setInitialState)では、ここで新宣言が
|
|
12925
14075
|
// 起動済みのため connectedCallback 末尾の startStreams を skip させる。
|
|
@@ -13057,6 +14207,36 @@ class State extends HTMLElementBase {
|
|
|
13057
14207
|
bindWebComponent(this, this._boundComponent, this._boundComponentStateProp, state);
|
|
13058
14208
|
}
|
|
13059
14209
|
}
|
|
14210
|
+
/**
|
|
14211
|
+
* Light DOM の mapped コンポーネントが、自分のサブツリーのバインディングを張る(§1.13)。
|
|
14212
|
+
*
|
|
14213
|
+
* Shadow DOM 形では子スコープが別 rootNode にあり、`setStateElementByName` の初回登録から
|
|
14214
|
+
* その root ぶんの `buildBindings` が別パスとして起動する。Light DOM ではホストと同じ root に
|
|
14215
|
+
* いるためそのパスが存在せず、かといってホストのパスに混ぜると `@name` の解決が
|
|
14216
|
+
* この要素の名前登録より先に来てしまう。そこで `getSubscriberNodes` がホスト側の走査から
|
|
14217
|
+
* このサブツリーを外し、名前登録が済んだここで同じことを自前で行う。
|
|
14218
|
+
*
|
|
14219
|
+
* `{{ }}` の変換だけはホストのパスが root 全体に対して済ませている(純粋にテキスト操作で
|
|
14220
|
+
* state に依存しないため)。構造フラグメントの収集は fragment info を rootNode + state 名で
|
|
14221
|
+
* 登録するので state 依存であり、ホストのパスからは外してここで走らせる。
|
|
14222
|
+
*
|
|
14223
|
+
* ループ文脈を null で渡すのは Shadow DOM 形(`initializeBindings(shadowRoot, null)`)と
|
|
14224
|
+
* 揃えるため —— 子孫の `getLoopContextByNode` はコンポーネント要素まで遡って
|
|
14225
|
+
* 親スコープの行を見つける。
|
|
14226
|
+
*/
|
|
14227
|
+
_initializeLightDomComponentScope() {
|
|
14228
|
+
const component = this._boundComponent;
|
|
14229
|
+
if (component === null || this.parentNode !== component) {
|
|
14230
|
+
// Shadow DOM 形(parentNode が ShadowRoot)は対象外
|
|
14231
|
+
return;
|
|
14232
|
+
}
|
|
14233
|
+
if (!component.hasAttribute(config.bindAttributeName)) {
|
|
14234
|
+
// plain 形はホストのパスに含まれたままなので、ここで張ると二重になる
|
|
14235
|
+
return;
|
|
14236
|
+
}
|
|
14237
|
+
collectStructuralFragments(this._rootNode, component);
|
|
14238
|
+
initializeBindings(component, null);
|
|
14239
|
+
}
|
|
13060
14240
|
/**
|
|
13061
14241
|
* mapped な `bind-component` が切断 → 再接続したときに、束ねているパスを読み直させる(§1.9)。
|
|
13062
14242
|
*
|
|
@@ -13158,6 +14338,9 @@ class State extends HTMLElementBase {
|
|
|
13158
14338
|
await this._initializeBindWebComponent();
|
|
13159
14339
|
await this._initialize();
|
|
13160
14340
|
this._initialized = true;
|
|
14341
|
+
// 名前登録(_initialize の末尾)が済んだこの時点でなければ、子スコープの
|
|
14342
|
+
// `@name` 参照が解決できない(§1.13)
|
|
14343
|
+
this._initializeLightDomComponentScope();
|
|
13161
14344
|
this._resolveInitialize?.();
|
|
13162
14345
|
}
|
|
13163
14346
|
else if (!this._dcc && getStateElementByName(this._rootNode, this._name) !== this) {
|
|
@@ -13209,6 +14392,19 @@ class State extends HTMLElementBase {
|
|
|
13209
14392
|
// _streamsStartedGeneration ガード: $connectedCallback 内の setInitialState
|
|
13210
14393
|
// (接続中の再 set)で _state セッター側が新宣言を起動済みの場合は skip する
|
|
13211
14394
|
// (skip しないと同一 connect サイクルで source が 2 回起動する、設計書 §2-3)。
|
|
14395
|
+
// $watch の有効化($connectedCallback 完了後 = 初期化中の書き込みは購読対象外)。
|
|
14396
|
+
// ガードは startStreams と同じ理由で必要(await 中の切断・再接続)。SSR では
|
|
14397
|
+
// 走らせない — ハンドラの副作用がサーバとクライアントで二重に実行されるため
|
|
14398
|
+
// (docs/state-watch-hook-design.md §11)。
|
|
14399
|
+
// startStreams より先に呼ぶ: stream の起動時書き込み(initial リセット・status 遷移)は
|
|
14400
|
+
// watch から観測できるべきで、逆向きは要らない。
|
|
14401
|
+
// 再入不要: 接続中の _state 再 set は _state セッター側で startWatch 済みだが、
|
|
14402
|
+
// startWatch は Set への add で冪等なので $streams のような世代ガードは要らない。
|
|
14403
|
+
if (!inSsr() &&
|
|
14404
|
+
this._rootNode !== null &&
|
|
14405
|
+
connectGeneration === this._connectGeneration) {
|
|
14406
|
+
startWatch(this);
|
|
14407
|
+
}
|
|
13212
14408
|
if (!inSsr() &&
|
|
13213
14409
|
this._rootNode !== null &&
|
|
13214
14410
|
connectGeneration === this._connectGeneration &&
|
|
@@ -13238,6 +14434,10 @@ class State extends HTMLElementBase {
|
|
|
13238
14434
|
// registry は残るため再接続後の初回アクセスで同内容の proxy が再生成される)。
|
|
13239
14435
|
abortAllStreams(this);
|
|
13240
14436
|
clearStreamNamespace(this);
|
|
14437
|
+
// watch は発火対象から外すだけで registry は保持する(stream の abortAllStreams と
|
|
14438
|
+
// 同じ二段構え、設計書 §9)。registry まで捨てると、_state セッターが再度走らない
|
|
14439
|
+
// 再接続で宣言を作り直せず watch が二度と発火しない。
|
|
14440
|
+
deactivateWatch(this);
|
|
13241
14441
|
this._rootNode = null;
|
|
13242
14442
|
}
|
|
13243
14443
|
}
|
|
@@ -13257,6 +14457,9 @@ class State extends HTMLElementBase {
|
|
|
13257
14457
|
get listKeys() {
|
|
13258
14458
|
return this._listKeys;
|
|
13259
14459
|
}
|
|
14460
|
+
get watchPaths() {
|
|
14461
|
+
return this._watchPaths;
|
|
14462
|
+
}
|
|
13260
14463
|
get elementPaths() {
|
|
13261
14464
|
return this._elementPaths;
|
|
13262
14465
|
}
|
|
@@ -13568,6 +14771,8 @@ const builtinFilterMeta = {
|
|
|
13568
14771
|
mul: { description: "乗算", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
|
|
13569
14772
|
div: { description: "除算", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
|
|
13570
14773
|
mod: { description: "剰余", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
|
|
14774
|
+
abs: { description: "絶対値", hasArgs: false, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 0 },
|
|
14775
|
+
clamp: { description: "範囲内に丸める (min,max)", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 2, maxArgs: 2, argTypes: ["number", "number"] },
|
|
13571
14776
|
// 数値フォーマット
|
|
13572
14777
|
fix: { description: "固定小数点表記", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
|
|
13573
14778
|
locale: { description: "ロケール形式で数値フォーマット", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["string"] },
|
|
@@ -13581,6 +14786,8 @@ const builtinFilterMeta = {
|
|
|
13581
14786
|
pad: { description: "パディング (length[,char])", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "string"] },
|
|
13582
14787
|
rep: { description: "繰り返し (count)", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
|
|
13583
14788
|
rev: { description: "文字順を反転", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
|
|
14789
|
+
truncate: { description: "切り詰めて省略記号 (length[,suffix])", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "string"] },
|
|
14790
|
+
join: { description: "配列を連結 ([separator])", hasArgs: true, resultType: "string", acceptTypes: ["array"], minArgs: 0, maxArgs: 1, argTypes: ["string"] },
|
|
13584
14791
|
// 数値パース・丸め
|
|
13585
14792
|
int: { description: "整数にパース", hasArgs: false, resultType: "number", acceptTypes: ["string", "number"], minArgs: 0, maxArgs: 0 },
|
|
13586
14793
|
float: { description: "浮動小数点数にパース", hasArgs: false, resultType: "number", acceptTypes: ["string", "number"], minArgs: 0, maxArgs: 0 },
|
|
@@ -13588,11 +14795,15 @@ const builtinFilterMeta = {
|
|
|
13588
14795
|
floor: { description: "切り下げ", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
|
|
13589
14796
|
ceil: { description: "切り上げ", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
|
|
13590
14797
|
percent: { description: "パーセンテージ形式", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
|
|
14798
|
+
// number だけでなく string も受ける。実用チェーンは fix / percent の後ろに繋がり、
|
|
14799
|
+
// それらは既に string を返すため(builtinFilters.ts の unit を参照)
|
|
14800
|
+
unit: { description: "単位(接尾辞)を付加", hasArgs: true, resultType: "string", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["string"] },
|
|
13591
14801
|
// 日付・時刻
|
|
13592
14802
|
date: { description: "ロケール形式の日付", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
|
|
13593
14803
|
time: { description: "ロケール形式の時刻", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
|
|
13594
14804
|
datetime: { description: "ロケール形式の日時", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
|
|
13595
14805
|
ymd: { description: "YYYY-MM-DD 形式", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
|
|
14806
|
+
hms: { description: "HH:MM:SS 形式", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
|
|
13596
14807
|
// 真偽値・変換
|
|
13597
14808
|
falsy: { description: "偽値か判定", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
|
|
13598
14809
|
truthy: { description: "真値か判定", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
|
|
@@ -13636,6 +14847,27 @@ function getWcsManifest() {
|
|
|
13636
14847
|
},
|
|
13637
14848
|
// 正本 STRUCTURAL_BINDING_TYPE_SET から導出(手書きの二重定義を排除)。
|
|
13638
14849
|
structuralDirectives: Array.from(STRUCTURAL_BINDING_TYPE_SET),
|
|
14850
|
+
modifiers: {
|
|
14851
|
+
flags: MODIFIER_FLAGS,
|
|
14852
|
+
keyValue: MODIFIER_KEYS,
|
|
14853
|
+
eventNamePrefix: EVENT_PROP_PREFIX,
|
|
14854
|
+
},
|
|
14855
|
+
indexParam: {
|
|
14856
|
+
prefix: INDEX_PARAM_PREFIX,
|
|
14857
|
+
maxDepth: MAX_WILDCARD_DEPTH,
|
|
14858
|
+
},
|
|
14859
|
+
bindingTypes: {
|
|
14860
|
+
elseKeyword: ELSE_KEYWORD,
|
|
14861
|
+
spread: SPREAD_PROP,
|
|
14862
|
+
eventPropertyPrefix: EVENT_PROP_PREFIX,
|
|
14863
|
+
propNamespaces: {
|
|
14864
|
+
eventToken: EVENT_TOKEN_NAMESPACE,
|
|
14865
|
+
command: COMMAND_NAMESPACE,
|
|
14866
|
+
class: CLASS_NAMESPACE,
|
|
14867
|
+
attr: ATTR_NAMESPACE,
|
|
14868
|
+
style: STYLE_NAMESPACE,
|
|
14869
|
+
},
|
|
14870
|
+
},
|
|
13639
14871
|
},
|
|
13640
14872
|
// 実装(Record のキー)から自動導出。手リストを持たない=ドリフトの構造的排除。
|
|
13641
14873
|
filters: Object.keys(outputBuiltinFilters),
|
|
@@ -13654,6 +14886,7 @@ function getWcsManifest() {
|
|
|
13654
14886
|
STATE_EVENT_TOKENS_NAME,
|
|
13655
14887
|
STATE_ON_NAME,
|
|
13656
14888
|
STATE_STREAMS_NAME,
|
|
14889
|
+
STATE_WATCH_NAME,
|
|
13657
14890
|
STATE_LIST_KEYS_NAME,
|
|
13658
14891
|
STATE_STREAM_STATUS_NAMESPACE_NAME,
|
|
13659
14892
|
STATE_STREAM_ERROR_NAMESPACE_NAME,
|