@wcstack/state 2.1.1 → 2.3.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 +283 -6
- package/README.md +284 -6
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.d.ts +295 -2
- package/dist/index.esm.js +2259 -179
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.esm.js +4 -0
- package/dist/parser.esm.js +21 -4
- package/dist/wcs-manifest.json +2 -0
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -114,10 +114,11 @@
|
|
|
114
114
|
- **event token** — command token の双対。wc-bindable 要素が dispatch するイベントを `eventToken.<prop>: tokenName` + `$on` マップで state が受信
|
|
115
115
|
- **stream** — `$streams` 宣言で連続的な非同期フロー(async iterable / `ReadableStream`)を fold して reactive プロパティ化。switchMap 型の依存駆動 restart 付き
|
|
116
116
|
- **パス getter** — ドットパスキー getter(`get "users.*.fullName"()`)によるデータツリーの任意の深さへのフラットな仮想プロパティ定義、自動依存追跡・キャッシュ
|
|
117
|
+
- **再帰パス** — `$recursion: { "nodes.*": "children.*" }` で木の形が繰り返す場所を宣言し、1 本の `**` getter(`get "nodes.**.total"()`)が全深さを覆う。`$getAll(path, [])` は全深さを合併し、`$setAll(path, [], value)` は全深さへブロードキャストする
|
|
117
118
|
- **Mustache 構文** — テキストノードでの `{{ path|filter }}`
|
|
118
119
|
- **複数の状態ソース** — JSON, JS モジュール, インラインスクリプト, API, 属性
|
|
119
120
|
- **SVG サポート** — `<svg>` 要素内でのフルバインディング対応
|
|
120
|
-
- **ライフサイクルフック** — `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback`、Web Component 用 `$stateReadyCallback`
|
|
121
|
+
- **ライフサイクルフック** — `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback` / `$errorCallback`、Web Component 用 `$stateReadyCallback`
|
|
121
122
|
- **headless な watch** — `$watch` はパスが描画されていてもいなくても state の変化で発火する
|
|
122
123
|
- **診断** — 解決しないパス・添字の本数・getter の循環を `@wcstack/lint`・VS Code 拡張と同じ診断 code で報告する
|
|
123
124
|
- **TypeScript サポート** — `defineState()` によるドットパス自動補完付き型付き状態定義([詳細](docs/define-state.ja.md))。`@wcstack/typescript` は同じ型を HTML の検証器へ運び(`wcs-schema`)、インライン state スクリプトを型検査する(`wcs-tsc`)— [docs/typescript.ja.md](../../docs/typescript.ja.md)
|
|
@@ -247,7 +248,7 @@
|
|
|
247
248
|
<div data-wcs="textContent: cart.total"></div>
|
|
248
249
|
```
|
|
249
250
|
|
|
250
|
-
ボリュームは getter・`$watch`・`$listKeys`・`$updatedCallback`・`$connectedCallback`/`$disconnectedCallback`
|
|
251
|
+
ボリュームは getter・`$watch`・`$listKeys`・`$updatedCallback`・`$connectedCallback`/`$disconnectedCallback` を宣言できます(すべてマウントパス相対)。`$errorCallback` はルート専用です(バインディングの失敗はツリーの所有者へ 1 回だけ報告されます)。読み込み順は自由です(ルートより先に接続されたボリュームは、ルートの登録時に接ぎ木されます)。マウントパスは静的パスのみです(`*`・`$`・`#`・`@` は不可)。初期化後に `mount` 属性を変更することはできません — 変更は console 警告付きで無視されます。要素を取り除き、望むパスで新しい要素を追加してください。
|
|
251
252
|
|
|
252
253
|
> **v1 の名前付き状態からの移行:** `<wcs-state name="cart">` + `total@cart` は `<wcs-state mount="cart">` + `cart.total` になります。v2 では `name` 属性は fail-fast し、パス中の `@` は parse error です(どちらもこの誘導文付き)。移行の対応表: [docs/state-mount-design.md](../../docs/state-mount-design.md) §9。
|
|
253
254
|
|
|
@@ -362,6 +363,8 @@ property[#modifier]: path[|filter[|filter(args)...]]
|
|
|
362
363
|
|
|
363
364
|
### バインディング authority (`#init=` / `#sync=`)
|
|
364
365
|
|
|
366
|
+
**これが解決する問題。** バインディングが attach する時点で既に値を持っている要素 —— 永続値をロード済みの `<wcs-storage>`、時計、自分のスナップショットを復元するウィジェット —— は state の初期値に上書きされます。双方向バインディングの初期同期が state→element に書くからです。そのバインディングにだけ `#init=element` を付けると、初期同期は*要素*が勝ちます。以後の変更は通常どおり両方向に流れます。このケース(load-before-bind)は下で具体的に説明します。この節の残りは、それを一例として含む一般規則です。
|
|
367
|
+
|
|
365
368
|
`static wcBindable` を宣言したカスタム要素への prop バインディングは、**authority**(バインディング attach 時の**初期同期**をどちら側が勝つか)を解決します。定常時の方向は authority とは別に、メンバの宣言形状で決まります: output-only メンバは state からの書き込みを恒久的に受け付けず(契約)、双方向メンバは初期同期の勝者と無関係に以後は両方向に流れます。既定 authority はメンバの宣言位置から導出されます(`enableDirectionalInitialSync` で既定 ON):
|
|
366
369
|
|
|
367
370
|
| メンバの宣言位置 | 既定 authority | 効果 |
|
|
@@ -377,7 +380,7 @@ property[#modifier]: path[|filter[|filter(args)...]]
|
|
|
377
380
|
|
|
378
381
|
要素が `properties[].event` を dispatch したとき、state に書かれる値は **`getter(event)`** です。`getter` を省略するとプロトコル既定の [`(e) => e.detail`](https://github.com/wc-bindable-protocol/wc-bindable-protocol/blob/main/SPEC.md#default-getter) が適用され、**`detail` 全体がそのまま**書かれます。このとき宣言したプロパティは要素から読まれ*ません*。イベントのペイロードが正です。`wcBindable` を持たない素の HTML 要素は逆で、`input`/`change` 時に `element[propName]` を読みます。
|
|
379
382
|
|
|
380
|
-
したがって `getter` なしで `detail: { value: 7654321 }` を dispatch する要素は、数値ではなく**オブジェクト** `{ value: 7654321 }` を state
|
|
383
|
+
したがって `getter` なしで `detail: { value: 7654321 }` を dispatch する要素は、数値ではなく**オブジェクト** `{ value: 7654321 }` を state に書きます。しかもこの失敗はほぼ無言です。書き戻し(`Number({ value: … })` → `NaN`)は例外を投げず、`@wcstack/lint` にも見えません(ペイロードの形は静的に分かりません)。ランタイムは、イベント時点で見分けられる 2 形についてだけ、要素 × プロパティごとに 1 回警告します(`wcs/default-getter-mismatch`): 要素プロパティに値があるのに `detail` が `undefined`(素の `Event`、または `detail` の付け忘れ)、および要素プロパティがオブジェクトでないのに `detail` が `<propName>` キーを持つオブジェクト(上のラッパー)。それ以外の不整合は気づかれずに通り、どちらの場合も書き込み自体はそのまま行われます。次の 2 形のどちらかに揃えてください:
|
|
381
384
|
|
|
382
385
|
```javascript
|
|
383
386
|
class YenInput extends HTMLElement {
|
|
@@ -944,6 +947,20 @@ export default {
|
|
|
944
947
|
|
|
945
948
|
getter の例外は握り潰されません。評価された場所(バインディングの適用・`$watch` の評価・自分での読み取り)でそのまま表面化します。
|
|
946
949
|
|
|
950
|
+
#### 依存追跡の境界
|
|
951
|
+
|
|
952
|
+
依存グラフに何が載るかは 3 つの規則で決まります。踏み越えるまで意識する必要はありませんが、踏み越えたときの症状は「値が更新されなくなる。エラーは出ない」なので、ここにまとめておきます:
|
|
953
|
+
|
|
954
|
+
| 規則 | 踏み越えたときの見え方 |
|
|
955
|
+
|---|---|
|
|
956
|
+
| **追跡されるのは `this` を通した *パス* の読み取りだけ。** `this.form` は `form` を、`this["form.name"]` は `form.name` を追跡する。`this.form.name` が追跡するのは **`form` だけ** —— `.name` は返ってきたオブジェクトへの素のプロパティアクセスでしかない。`Date.now()`・DOM・モジュール変数・クロージャで掴んだオブジェクトは何も登録しない | その入力に対して getter は二度と再評価されず、最初の値が残り続ける(上の例)。`this.form.name` を読む getter は、`<input data-wcs="value: form.name">` を編集しても再実行されない —— `this["form.name"]` で読む |
|
|
957
|
+
| **setter の中の読み取りは追跡しない。** setter は命令的な代入であって派生ではないので、その中で読んだものは何の依存にもならない | 何を書くかを `this.a` を読んで決める setter は、`a` が変わっても再実行されない。再実行されるのは getter だけ |
|
|
958
|
+
| **同値ガードはプリミティブにだけ効く。** 現在値と `Object.is` で等しいプリミティブの書き込みはキューに入る前に落とされる。オブジェクト・配列の書き込みは同じ参照でも必ず通る | 同じ文字列を再代入しても何も起きない。同じオブジェクトを再代入するとバインディングと `$watch` が再発火する(`config.sameValueGuard`。`semantics: "event"` のプロパティはどちらにせよ対象外) |
|
|
959
|
+
|
|
960
|
+
規則 1 は静的解析で捕まえられる唯一の規則です。getter が `this.form.name` を読んでいて、ドキュメントのどこかで `form.name` を書いている(`value:` バインド・spread・`this["form.name"] = …`)と、`wcs-validate` と VS Code 拡張が `wcs/getter-untracked-read` を報告します。ルートを丸ごと置換するだけの設計(router の params・`$streams` の fold)には出ません。
|
|
961
|
+
|
|
962
|
+
`$untrackDependency(fn)` は setter の規則を getter に意図的に適用するもので、`fn` の中の読み取りは追跡されません。`$trackDependency(path)` は最初の規則に対する逃げ道です。
|
|
963
|
+
|
|
947
964
|
### ループインデックス変数(`$1`, `$2`, ...)
|
|
948
965
|
|
|
949
966
|
getter やイベントハンドラ内で、`this.$1`、`this.$2` などで現在のループイテレーションのインデックスを取得できます(0始まりの値、1始まりの命名):
|
|
@@ -1086,6 +1103,207 @@ export default {
|
|
|
1086
1103
|
};
|
|
1087
1104
|
```
|
|
1088
1105
|
|
|
1106
|
+
## 再帰パス(`$recursion`)
|
|
1107
|
+
|
|
1108
|
+
パスは深さを文字列に焼き付けます。`nodes.*.children.*.total` はワイルドカードちょうど 2 段のパスであり、木が 1 段深くなっても 3 段には伸びません。しかし木の深さはコードではなく**データの性質**です。`$recursion` はこの隔たりを埋めます。形が繰り返す場所を宣言し、あとは「いま何段目であれ」を `**` と書きます。
|
|
1109
|
+
|
|
1110
|
+
```javascript
|
|
1111
|
+
export default {
|
|
1112
|
+
$recursion: { "nodes.*": "children.*" }, // アンカー → 反復サブパス
|
|
1113
|
+
|
|
1114
|
+
nodes: [
|
|
1115
|
+
{ value: 1, selected: false, children: [
|
|
1116
|
+
{ value: 10, selected: false, children: [
|
|
1117
|
+
{ value: 100, selected: false, children: [] }
|
|
1118
|
+
]},
|
|
1119
|
+
{ value: 20, selected: false, children: [] }
|
|
1120
|
+
]},
|
|
1121
|
+
{ value: 2, selected: false, children: [] }
|
|
1122
|
+
],
|
|
1123
|
+
|
|
1124
|
+
// getter は 1 本で全深さぶん。`**` は評価されている深さに束縛される
|
|
1125
|
+
get "nodes.**.total"() {
|
|
1126
|
+
return this["nodes.**.value"]
|
|
1127
|
+
+ this.$getAll("nodes.**.children.*.total").reduce((a, b) => a + b, 0);
|
|
1128
|
+
},
|
|
1129
|
+
|
|
1130
|
+
// 木全体の集計。`[]` は全深さの合併
|
|
1131
|
+
get treeTotal() {
|
|
1132
|
+
return this.$getAll("nodes.**.value", []).reduce((a, b) => a + b, 0);
|
|
1133
|
+
},
|
|
1134
|
+
|
|
1135
|
+
clearSelection() {
|
|
1136
|
+
this.$setAll("nodes.**.selected", [], false);
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
この森の total は `131 / 110 / 100 / 20 / 2`、`treeTotal` は `133` になります。
|
|
1142
|
+
|
|
1143
|
+
**`**` はオーサリング層だけの記号で、エンジンには決して降りません。** 具体パス(`nodes.*.children.*.total`)を読んだ時点で、その深さの getter が遅延実体化されます(実際に触れた深さのぶんだけアクセサが生えます)。その先 —— `PathInfo`、依存グラフ、`$1`…`$n`、`$resolve`、リスト差分 —— が見るのは、いつもどおりワイルドカード本数が固定された普通のパスです。リアクティブの中核は新しい形を覚えていません。
|
|
1144
|
+
|
|
1145
|
+
### 再帰点を宣言する
|
|
1146
|
+
|
|
1147
|
+
`$recursion` は 1 つの**アンカー**を、1 段深くする**反復サブパス**へ対応づけます。どちらも「固定プロパティ列 + 末尾の `.*`」の形で、リストそのものではなくリストの**要素**を名指します:
|
|
1148
|
+
|
|
1149
|
+
```javascript
|
|
1150
|
+
$recursion: { "nodes.*": "children.*" } // nodes[i].children[j].children[k]…
|
|
1151
|
+
$recursion: { "data.tree.*": "kids.*" } // 深い位置のアンカーも可
|
|
1152
|
+
$recursion: { "nodes.*": "nodes.*" } // 自己相似な綴りも可
|
|
1153
|
+
```
|
|
1154
|
+
|
|
1155
|
+
`**` に意味を与えるのはこの宣言だけです。`$recursion` の無い state では `**` はパスの文字ですらなく(`wcs/recursion-unsupported`)、この記法が子孫検索へ黙って滑り落ちることはありません。このバージョンが受け付けるのは **state ごとに単一の自己再帰アンカー**です。アンカーの途中のワイルドカード、2 つ目のエントリ、2 つのアンカー間の相互再帰、1 本のパスに 2 つ目の `**`、`get "nodes.**"`(これはノード自身であって、ノード配下の計算パスではありません)、接尾辞が構造そのものを名指す `**` getter(`get "nodes.**.children"()` / `.children.*` / `.children.length` —— 全深さで実データの子リストを影にしてしまいます)、同じ具体パスへ展開される 2 本の `**` getter、そして再帰 **setter** は、宣言を読んだ時点で拒否します —— 別の意味に解釈することはありません。
|
|
1156
|
+
|
|
1157
|
+
宣言が定義する族は無限ですが、state に生えるのは実際に要求された深さだけです:
|
|
1158
|
+
|
|
1159
|
+
```
|
|
1160
|
+
k=0 nodes.*
|
|
1161
|
+
k=1 nodes.*.children.*
|
|
1162
|
+
k=2 nodes.*.children.*.children.*
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
### `**` はどこで何を意味するか
|
|
1166
|
+
|
|
1167
|
+
`**` は深さを表す変数で、**束縛**されるか**合併**されるかは文脈が決めます。これは新しい規則ではなく、`*` が既に持っている「現在行」と「全行」の書き分けをそのまま継いだものです:
|
|
1168
|
+
|
|
1169
|
+
| `**` が現れる場所 | 意味 |
|
|
1170
|
+
|---|---|
|
|
1171
|
+
| getter のキー(`get "nodes.**.total"()`) | 評価されている深さに束縛 |
|
|
1172
|
+
| その getter 本体でのパス読み(`this["nodes.**.value"]`) | 同じ深さに束縛 |
|
|
1173
|
+
| `$getAll(path)`(添字**省略**) | その深さに束縛。展開されるのは `**` より**後ろ**のワイルドカードだけ |
|
|
1174
|
+
| `$getAll(path, [])`(**明示**) | **全深さの合併** —— 深さ優先・行きがけ・添字昇順 |
|
|
1175
|
+
| `$getAll(path, [i, …])` | 拒否。接頭辞ではどの深さの話か言えない(`wcs/recursion-getall-form`) |
|
|
1176
|
+
| `$setAll(path, [], value)` | 全深さへのブロードキャスト(合併と同じ走査・同じ順序) |
|
|
1177
|
+
| `$resolve` / `$postUpdate` / `$trackDependency` / `$watch` のキー / `$listKeys` のキー / markup の `data-wcs` / 直接代入 | 拒否(`wcs/recursion-unsupported`) |
|
|
1178
|
+
|
|
1179
|
+
束縛形は束縛先の深さを必要とするので、**再帰 getter の中**でしか解決できません(アンカー配下の普通の行 getter や、その行に紐づくイベントハンドラも同じく実体の `ListIndex` を持つので使えます)。トップレベルで `this["nodes.**.value"]` を読むと `wcs/recursion-context` になります —— どのノードのつもりだったかを黙って推測することはありません。深さは行の添字と同じく**最も内側の評価フレームだけ**から読みます。再帰 getter が呼ぶ普通の getter(`get "nodes.**.x"() { return this.helper }` と `get helper() { return this["nodes.**.value"] }`)は自分の行を持たないので、これも `wcs/recursion-context` になります —— `**` は再帰 getter の側で読み、値を渡してください。合併形は深さを要求しないので、トップレベルの getter でも普通の行 getter でもメソッドでも読めます。
|
|
1180
|
+
|
|
1181
|
+
```javascript
|
|
1182
|
+
this.$getAll("nodes.**.value", []); // [1, 10, 100, 20, 2] —— 深さ優先・行きがけ
|
|
1183
|
+
```
|
|
1184
|
+
|
|
1185
|
+
`**` より**後ろ**の `*` は、各ノードで固定本数のパスと同じ順に展開し、そのノードの分を出し切ってから子へ降ります。上の木のノードに `tags` があるとき(`1` → `[3, 4]`、`10` → `[5]`、`20` → `[7]`、他は空):
|
|
1186
|
+
|
|
1187
|
+
```javascript
|
|
1188
|
+
this.$getAll("nodes.**.tags.*.v", []); // [3, 4, 5, 7] —— ノード 1 の tags、次にノード 10 の、次にノード 20 の
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1191
|
+
### 孫を二重に数えない集計
|
|
1192
|
+
|
|
1193
|
+
木を畳むのは再帰 getter なので、この書き分けが集計の成否そのものになります:
|
|
1194
|
+
|
|
1195
|
+
```javascript
|
|
1196
|
+
// ✅ 省略 —— この深さに束縛されるので、直下の子だけを合計する
|
|
1197
|
+
get "nodes.**.total"() {
|
|
1198
|
+
return this["nodes.**.value"]
|
|
1199
|
+
+ this.$getAll("nodes.**.children.*.total").reduce((a, b) => a + b, 0);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// ❌ `[]` —— 全深さの子 total。各ノードの total が自分の子孫の total を再び含み、
|
|
1203
|
+
// getter が自分自身を要求することになる。実際には誤った値ではなく
|
|
1204
|
+
// `wcs/getter-cycle` になる。
|
|
1205
|
+
get "nodes.**.total"() {
|
|
1206
|
+
return this["nodes.**.value"]
|
|
1207
|
+
+ this.$getAll("nodes.**.children.*.total", []).reduce((a, b) => a + b, 0);
|
|
1208
|
+
}
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
同じ間違いを再帰の**外側**でやると、こちらは静かです。踏む循環が無く、もっともらしい大きすぎる値が返るだけになります。集計値の合併は、孫を「親の total の内訳」として 1 回、「合併の要素」としてもう 1 回数えます:
|
|
1212
|
+
|
|
1213
|
+
```javascript
|
|
1214
|
+
// ❌ 363 —— 全ノードの total を合併しているが、total は既に部分木を含んでいる
|
|
1215
|
+
get treeTotalWrong() {
|
|
1216
|
+
return this.$getAll("nodes.**.total", []).reduce((a, b) => a + b, 0);
|
|
1217
|
+
}
|
|
1218
|
+
// ✅ 133 —— 生の値を合併する
|
|
1219
|
+
get treeTotal() {
|
|
1220
|
+
return this.$getAll("nodes.**.value", []).reduce((a, b) => a + b, 0);
|
|
1221
|
+
}
|
|
1222
|
+
// ✅ 133 —— あるいはルートだけを足す(各ルートの total が既に部分木を畳んでいる)
|
|
1223
|
+
get treeTotalFromRoots() {
|
|
1224
|
+
return this.$getAll("nodes.*.total", []).reduce((a, b) => a + b, 0);
|
|
1225
|
+
}
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
**合併するのは生の値か、さもなくばルートだけを足すこと。自分の部分木を既に集計している値を合併してはいけません。** 二重計上かどうかはパス文字列からは決定できないので、これを捕まえると約束する診断はありません。
|
|
1229
|
+
|
|
1230
|
+
### 書き込みはブロードキャストのみ
|
|
1231
|
+
|
|
1232
|
+
再帰 `$setAll` が受け付けるのは `[]` + 素の値という 1 つの形だけで、戻り値は書き込んだアドレスの件数です(上の森なら 5):
|
|
1233
|
+
|
|
1234
|
+
```javascript
|
|
1235
|
+
this.$setAll("nodes.**.selected", [], false); // 全深さの全ノード
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
それ以外の形は、走査が 1 件でも書く**前に**拒否します。拒否された呼び出しは木を一切変更しません。この保証は**形**の検査についてのもので、添字綴りの葉(`nodes.**.children.0.value`)は**データ**の条件で途中で止まることがあります —— `children` が空のノードには書き込む子 `0` が無いためで、固定本数の `$setAll("nodes.*.children.0.value", [], v)` と同じ振る舞いです:
|
|
1239
|
+
|
|
1240
|
+
| 形 | 拒否する理由 |
|
|
1241
|
+
|---|---|
|
|
1242
|
+
| 非空の接頭辞 | 接頭辞ではどの深さに適用されるのか言えない(`wcs/recursion-setall-form`) |
|
|
1243
|
+
| 添字の省略 | 書き込み API は文脈を取らないので束縛する深さが無い —— `[]` を渡す |
|
|
1244
|
+
| mapper 関数 | `(current, ...indexes)` の添字の本数が深さごとに変わる |
|
|
1245
|
+
| `{ spread: true }` | 1 次元配列を木へ配るには作者が走査順を知っている必要があり、契約として使えない |
|
|
1246
|
+
| `nodes.**` / `nodes.**.children` / `nodes.**.children.*` / `nodes.**.children.length` —— 反復サブパスが多段(`branch.children.*`)なら、子リストへ至る途中の `nodes.**.branch` も。添字綴りも同じ形に畳まれる(`nodes.**.children.0` は子ノード、`nodes.**.children.0.total` は getter) | 構造そのものへの書き込み(`length` への代入はリストを切り詰める)は、その書き込み自身が確定済みの子アドレスを壊す(`wcs/recursion-structural-write`) |
|
|
1247
|
+
| `nodes.**.total`、およびその値の内側を指すパス | 再帰 getter に setter は無い。導出元を書く(`wcs/recursion-readonly`) |
|
|
1248
|
+
|
|
1249
|
+
読み取り専用の規則は `**` の綴りに依存しません。再帰 getter の具体的な展開形 —— `nodes.*.total` / `nodes.*.children.*.total` / … —— への書き込みも、固定本数の `$setAll` でも値付きの `$resolve(path, indexes, value)` でも直接代入でも、またその深さが実体化済みかどうかに関わらず、書き込みの入口で拒否します。この検査が無かったときは、未実体化の展開形が「無いキー」に見えてノードのオブジェクトに書き込まれ、代入値が getter のキャッシュ結果として固定されていました。
|
|
1250
|
+
|
|
1251
|
+
### 入力は木でなければならない
|
|
1252
|
+
|
|
1253
|
+
走査は深さ方向に降りながら、必要な形をその場で検査します。**同じ配列インスタンス**に 2 度到達したら拒否します。その配列が現在のノードの祖先のものなら循環(`wcs/recursion-cycle`)、そうでなければ 2 つのノードが 1 本の子リストを共有しています(`wcs/recursion-shared-list`)。各ノードに自分の `children` 配列を持たせてください —— **空**配列の使い回しは行を持たず別名化のしようがないので、追跡もせず正当です。
|
|
1254
|
+
|
|
1255
|
+
**走査は受け付けるのにエンジンがまだ追えない形が 1 つあります —— 行オブジェクトを作り直して `children` 配列を引き継ぐ置換です。** `this.nodes = this.nodes.map(n => ({ ...n }))` の後も子リストの台帳は配列をキーにしたままなので、その行は**旧**行オブジェクトに結び付いたままになります。その行の集計を一度読んだあとに、その下の葉を更新すると、その行の `nodes.*.total` だけが古いまま残ります —— 葉・より深い集計・すべての `[]` 合併は正しいので、何も警告は出ません。行は path 経由で in-place に書く(`$resolve` / `$setAll`)、行オブジェクトを引き継ぐ(`[...this.nodes]`)、部分木ごと置き換える(深いクローン)のいずれかにすると集計が追従します。これは `**` ではなくリストの同一性の制限で、手書きの `nodes.*.total` / `nodes.*.children.*.total` getter でも同じ挙動になります([#256](https://github.com/wcstack/wcstack/issues/256))。
|
|
1256
|
+
|
|
1257
|
+
上限は展開後のパスの**ワイルドカード 128 段**です。上の集計 getter は評価中のノードより 1 段下を読むので、127 段の鎖までは畳めて、128 段で `wcs/recursion-depth-exceeded` になります(アンカー・到達した深さ・組み立てようとしたパス・上限を名指しします)。この検査は getter 評価スタック自身の 128 段の上限(`wcs/getter-depth-exceeded`)より先に効くので、深い木は「深い」と報告され、循環の疑いを掛けられることはありません。途中で打ち切ることもしません —— 部分的な集計は、誤った値を正しい値として返すことだからです。
|
|
1258
|
+
|
|
1259
|
+
### 木を描画する
|
|
1260
|
+
|
|
1261
|
+
`**` は markup には書けず、再帰 `<template>` もありません。木は**自己参照コンポーネント**で描画します —— 子ごとに自分自身を shadow の中でマウントするカスタム要素 1 つです。各スコープの中で使うパスは常に 1 段だけ(`node.children.*`)なので markup が深さに依存せず、`node.total` はマウントを通ってルート state の再帰 getter に解決されるので、各ノードが自分の部分木の集計を表示できます。
|
|
1262
|
+
|
|
1263
|
+
```html
|
|
1264
|
+
<!-- ホスト側 -->
|
|
1265
|
+
<template data-wcs="for: nodes">
|
|
1266
|
+
<tree-node data-wcs="state.node: nodes.*"></tree-node>
|
|
1267
|
+
</template>
|
|
1268
|
+
```
|
|
1269
|
+
|
|
1270
|
+
```javascript
|
|
1271
|
+
const markup = `
|
|
1272
|
+
<wcs-state bind-component="state"></wcs-state>
|
|
1273
|
+
<span data-wcs="textContent: node.label"></span>
|
|
1274
|
+
<span data-wcs="textContent: node.total"></span>
|
|
1275
|
+
<template data-wcs="for: node.children">
|
|
1276
|
+
<tree-node data-wcs="state.node: node.children.*"></tree-node>
|
|
1277
|
+
</template>`;
|
|
1278
|
+
|
|
1279
|
+
customElements.define("tree-node", class extends HTMLElement {
|
|
1280
|
+
state = {}; // ← `node` を自分で持たない(マウントから届く)
|
|
1281
|
+
constructor() { super(); this.attachShadow({ mode: "open" }); }
|
|
1282
|
+
connectedCallback() { // ← shadow は constructor ではなくここで組む
|
|
1283
|
+
if (this.shadowRoot.childNodes.length === 0) this.shadowRoot.innerHTML = markup;
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
```
|
|
1287
|
+
|
|
1288
|
+
ここには 2 つの罠があり、どちらも実際に踏んだものです:
|
|
1289
|
+
|
|
1290
|
+
- **コンポーネントの `state` に、マウント先と同名のキーを置かないこと。** 無関係なメソッドや私有キーは構いませんが、自分の `node` を持つとマウントを隠し、子は自分の既定値を表示したまま一段も降りません(実行時に `wcs/mount-own-key-shadow` で名指されます)。
|
|
1291
|
+
- **shadow は constructor ではなく `connectedCallback` で組むこと。** constructor で `innerHTML` を入れると、`<template>` の中身を inert に保たない実装ではその中の要素まで upgrade され、自己参照コンポーネントは自分の constructor の中で無限再帰します。実ブラウザは通ってしまうので、素直なクラッシュではなく環境依存の地雷になります。
|
|
1292
|
+
|
|
1293
|
+
深さが固定なら、ここまでは要りません。展開後のパスは普通のパスなので、入れ子の `for` テンプレートから `nodes.*.total` や `nodes.*.children.*.total` を他と同じようにバインドできます。
|
|
1294
|
+
|
|
1295
|
+
### このバージョンに含まれないもの
|
|
1296
|
+
|
|
1297
|
+
以下はいずれも診断になります。黙って別の意味に解釈されることはありません。
|
|
1298
|
+
|
|
1299
|
+
- 複数アンカー、相互再帰、アンカー途中のワイルドカード、1 本のパスに 2 つ目の `**`
|
|
1300
|
+
- 再帰 setter、接尾辞が構造そのものを名指す `**` getter(`get "nodes.**.children"()`)、`**` getter の展開形と同名の具体 getter、そして代入による `**` 経由の書き込み(`this["nodes.**.x"] = v`、`++` も含む)
|
|
1301
|
+
- 再帰 `$setAll` の mapper・`{ spread: true }`・添字省略・非空の接頭辞・配列でない `indexes`。書き込み API には深さを束縛する評価文脈が無いので、`[]` は必須です
|
|
1302
|
+
- 再帰 `$getAll` の非空の接頭辞・配列でない `indexes`。**添字省略は正当です** —— 再帰 getter の中では束縛形で、評価中の深さを読みます
|
|
1303
|
+
- `data-wcs` / `$watch` や `$listKeys` のキー / `$resolve` / `$postUpdate` / `$trackDependency` への `**`
|
|
1304
|
+
- ボリューム(`mount=`)やマウントされたコンポーネント(`bind-component`)の `$recursion` と `**` getter —— ルートの state に置きます
|
|
1305
|
+
- 再帰 `<template>`、`$depth` 変数、公開の `maxDepth` オプション(3 つとも存在しません)
|
|
1306
|
+
|
|
1089
1307
|
## イベントハンドリング
|
|
1090
1308
|
|
|
1091
1309
|
`on*` プロパティでイベントハンドラをバインドします:
|
|
@@ -1334,6 +1552,23 @@ customElements.define("user-card", UserCard);
|
|
|
1334
1552
|
> `$postUpdate` はコンポーネント自身の語彙で書けます — パスはマウント先へ翻訳され、ホスト行の
|
|
1335
1553
|
> 添字は自動で前置されます。
|
|
1336
1554
|
|
|
1555
|
+
#### 公開 getter(コンポーネントの getter を外から読む)
|
|
1556
|
+
|
|
1557
|
+
マウントされたコンポーネントの getter はマウント先に**公開**されます: **ツリーに無いキーの読みは、その位置にマウントされたコンポーネントの getter で答える。ツリーにあるキーはツリーが勝つ。私有キーとメソッドは見せない。** 上の `user-card` なら、ホスト側のマークアップからコンポーネントの派生値をそのまま読めます:
|
|
1558
|
+
|
|
1559
|
+
```html
|
|
1560
|
+
<user-card data-wcs="state: user"></user-card>
|
|
1561
|
+
<span data-wcs="textContent: user.display"></span> <!-- "Alice <alice@example.com>" — コンポーネントの getter -->
|
|
1562
|
+
```
|
|
1563
|
+
|
|
1564
|
+
- 行マウントは行ごとに公開されます: `$getAll("users.*.display")` や同じ `for` 内の `text: .display` は各行のコンポーネントの getter を読みます。依存も流れます — `user.name` が変われば `user.display` を読んだものはすべて再描画されます
|
|
1565
|
+
- `get "children.*.label"()` のようにコンポーネント内のパスにワイルドカードを含む accessor は、内部では使えますが**公開されません**。各子行にマウントしたコンポーネントの `get label()` として定義してください。公開対象は、公開パスのワイルドカード数がマウント先のワイルドカード数と一致する accessor だけです
|
|
1566
|
+
- 親は子コンポーネントの登録より先に評価されるので、初回の読みは `undefined` になりえます。コンポーネントがマウントされ次第、値は収束します。派生式は `(x ?? 0)` のように防御的に書いてください
|
|
1567
|
+
- 未存在パスの警告は `getBindingsReady` とは独立に 1 マクロタスク(`setTimeout(0)`)だけ遅延します。autoloader や遅延したカスタム要素定義を使う場合、最終的にバインドが解決しても、登録前の初回警告が出ることがあります
|
|
1568
|
+
- ツリーに同名のキーがある場合(継承したプロパティも含む)はツリーが勝ち、ランタイムが 1 回だけ warn します(`wcs/mount-export-shadowed`)。同一インスタンスに同名キーを公開するコンポーネントが 2 つある形は設定ミスで、候補走査時に検出して throw します(`wcs/mount-export-ambiguous`)。検証付きキャッシュに命中すると他の候補は再走査しないため、初回解決後に追加した競合コンポーネントは検出されない場合があります
|
|
1569
|
+
- 公開キーへの外からの書き込みは accessor の setter を実行し、getter しか無ければ throw します(getter を隠すキーをツリーに作ることはありません)。`in` は公開キーを見ません
|
|
1570
|
+
- **自己再帰コンポーネント**(深さが有界でない木)が書けるようになります: 自分自身を `<template data-wcs="for: children"><tree-node data-wcs="state: ."></tree-node></template>` で入れ子にするコンポーネントは、`get total() { return this.value + this.$getAll("children.*.total").reduce((a, b) => a + (b ?? 0), 0); }` と各段 1 段の式で書け、再帰は台帳が解きます。パス自体は再帰を表現できない(ワイルドカードの個数が固定)ので、再帰は DOM に置き、パスはその展開形になります。設計: [docs/state-overlay-export-design.md](../../docs/state-overlay-export-design.md)
|
|
1571
|
+
|
|
1337
1572
|
### 独立した Web Component への状態注入(`__e2e__/single-component`)
|
|
1338
1573
|
|
|
1339
1574
|
ホストの外部状態に依存しないコンポーネントでも、`bind-component` で `state` を注入してリアクティブにできます。
|
|
@@ -2120,7 +2355,7 @@ export default {
|
|
|
2120
2355
|
|
|
2121
2356
|
## ライフサイクルフック
|
|
2122
2357
|
|
|
2123
|
-
状態オブジェクトに `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback`
|
|
2358
|
+
状態オブジェクトに `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback` / `$errorCallback` を定義すると、初期化・クリーンアップ・更新時・バインディング失敗時のフックとして利用できます。
|
|
2124
2359
|
|
|
2125
2360
|
```html
|
|
2126
2361
|
<wcs-state>
|
|
@@ -2150,6 +2385,7 @@ export default {
|
|
|
2150
2385
|
| `$connectedCallback` | 初回接続時は状態初期化後、再接続時は毎回呼び出し | 可(await される) |
|
|
2151
2386
|
| `$disconnectedCallback` | 要素が DOM から削除された時 | 不可(同期のみ) |
|
|
2152
2387
|
| `$updatedCallback(paths, indexesListByPath)` | live binding に更新が適用された後に呼び出し | 可(await されない) |
|
|
2388
|
+
| `$errorCallback(error, info)` | バインディングの適用に失敗した drain の後 — 失敗した本数ぶん、`$updatedCallback` の後に呼び出し | 可(await されない) |
|
|
2153
2389
|
|
|
2154
2390
|
`$disconnectedCallback` を除くすべてのフックで `async` を使用できます。リアクティブ Proxy はすべてのプロパティへの代入を変更として検知します。そのため、標準の `async/await` による処理とプロパティへの直接代入だけで非同期ロジックが完結します。ローディングフラグの切り替え、取得したデータの格納、エラーメッセージの更新といった処理もすべて単なるプロパティ代入で行えるため、非同期状態を管理するための複雑な抽象化機能は必要ありません。
|
|
2155
2391
|
|
|
@@ -2157,6 +2393,19 @@ export default {
|
|
|
2157
2393
|
- `$connectedCallback` は要素が接続される**たびに**呼ばれます(一度削除された後の再接続も含みます)。再確立が必要なセットアップ処理に適しています。
|
|
2158
2394
|
- `$disconnectedCallback` は同期的に呼び出されます。タイマーのクリア、イベントリスナーの削除、リソースの解放といったクリーンアップ処理に使用してください。
|
|
2159
2395
|
- `$updatedCallback(paths, indexesListByPath)` は、その drain で live binding が適用された path の一覧を受け取ります。binding のない state 書き込みでは呼ばれず、`paths` にも現れません。ワイルドカードをもつパスが更新された場合は、`indexesListByPath` から対象のインデックス情報も取得可能です。マウントされたコンポーネントのマーカーパス(`#m…`)は `paths` に現れません — コンポーネントの私有キーは私有のままです(DevTools の overlays 表示で見えます)。`async` を使用できますが、戻り値は await されません。
|
|
2396
|
+
- `$errorCallback(error, info)` はバインディングの**ページ内エラー境界**です。バインディングの適用が throw したとき(パス getter やフィルタが throw した、構造ディレクティブが失敗した)、その失敗は隔離され(同じバッチの残りは適用され、値も DOM も巻き戻されません)、このフックが無ければ `console.error` で報告されます。フックを宣言すると報告はそこへ届きます: `error` は throw された値、`info` はバインディングを識別する `{ path, bindingType, node }`(`path` は `data-wcs` に書いた形のまま。ワイルドカードもそのまま)。`this` は書き込み可能な state proxy なので、メッセージを state に書いて普通に描画するのが基本形です:
|
|
2397
|
+
|
|
2398
|
+
```js
|
|
2399
|
+
export default {
|
|
2400
|
+
user: null, loadError: "",
|
|
2401
|
+
get title() { return this.user.profile.name; }, // user が null の間は throw する
|
|
2402
|
+
$errorCallback(error, { path }) {
|
|
2403
|
+
this.loadError = `${path}: ${error.message}`; // <p data-wcs="textContent: loadError">
|
|
2404
|
+
},
|
|
2405
|
+
};
|
|
2406
|
+
```
|
|
2407
|
+
|
|
2408
|
+
フックはバッチの後(`$updatedCallback` の後)に走り、await されず、フック内で throw しても console に報告されるだけで drain は壊れません。DevTools にはフックの有無に関わらず全失敗が `state:binding-apply-error` として届きます。ルート専用で、ボリューム(`<wcs-state mount>`)に宣言しても無視されます。`$watch` ハンドラの失敗(別途隔離・報告)や、`$connectedCallback` / `$updatedCallback` が投げた例外(loud に失敗する)は対象外です。
|
|
2160
2409
|
- Web Component を使用している場合は、コンポーネント側に `async $stateReadyCallback(stateProp)` を定義おくことで、`bind-component` でバインドした状態が利用可能になった瞬間にフックとして呼び出されます。
|
|
2161
2410
|
|
|
2162
2411
|
## 遷移アニメーション
|
|
@@ -2214,13 +2463,26 @@ dropped. Validate statically: npx @wcstack/lint <file>.
|
|
|
2214
2463
|
|
|
2215
2464
|
### 添字の本数・階数・循環も検査されます
|
|
2216
2465
|
|
|
2217
|
-
パス文字列から機械的に決まる整合は、実行時にも lint にも同じ診断 code
|
|
2466
|
+
パス文字列から機械的に決まる整合は、実行時にも lint にも同じ診断 code で現れます。ただし下表の 6 つ —— `wcs/getter-depth-exceeded` / `wcs/index-param-range` / `wcs/recursion-context` / `wcs/recursion-shared-list` / `wcs/recursion-cycle` / `wcs/recursion-depth-exceeded` —— はこのリリースでは**実行時専用**で、lint は出しません。
|
|
2218
2467
|
|
|
2219
2468
|
| 診断 | 何を見るか | 直し方 |
|
|
2220
2469
|
|---|---|---|
|
|
2221
2470
|
| `wcs/index-arity` | `$resolve(path, indexes)` は `*` の本数と**厳密一致**、`$getAll(path, indexes)` / `$setAll(path, indexes, …)` は**上限**(不足は「残りの階層を全展開」という正当な接頭辞) | 本数を合わせる |
|
|
2222
2471
|
| `wcs/wildcard-rank` | パスの `*` の本数(と `$N` の N)が、囲む `for` の段数を超えていないか | `for` を足すか、`$resolve(path, indexes)` で行を明示する |
|
|
2223
|
-
| `wcs/getter-cycle` | パス getter
|
|
2472
|
+
| `wcs/getter-cycle` | パス getter どうしが循環参照していないか。実行時は「アドレススタックが既に積んでいるアドレスへ戻る」ことで判定する | 循環を断つ |
|
|
2473
|
+
| `wcs/getter-depth-exceeded` | getter の評価が 1 パスで評価できる深さ(128 段)を超え、かつ同じアドレスを 2 度通っていない = データが単に深い | 集計の段数を減らすか、木を平らにする |
|
|
2474
|
+
| `wcs/index-param-range` | `$N` は実在するワイルドカード段を指すこと(`$1`〜`$128`・先頭ゼロ不可) | 実在する段を使う |
|
|
2475
|
+
| `wcs/recursion-unsupported` | `**` を解釈しない場所へ `**` が渡った —— markup・`$watch` / `$listKeys` のキー・`$resolve` / `$postUpdate` / `$trackDependency`・代入、あるいは state が `$recursion` を宣言していない | 具体パスを使うか、アンカーを宣言する |
|
|
2476
|
+
| `wcs/recursion-declaration-invalid` | `$recursion` 宣言か `**` getter のキーが、このバージョンが受け付けない形 —— 要素を指さない・途中に添字セグメントを持つ(`"nodes.0.items.*"`)アンカー / 反復サブパス、複数アンカー、getter でない・setter を持つ `**` キー、`get "nodes.**"`、構造を名指す getter、同じ具体パスへ展開する 2 本の getter、展開形と同名の具体 getter。lint が先に出し、実行時は宣言を読んだ時点で throw する | 文面のとおり宣言を直す |
|
|
2477
|
+
| `wcs/recursion-anchor` | 宣言済みのアンカーと合致しない `**` パス(このバージョンは state ごとに単一の自己再帰アンカー)、または `**` の後ろが整形されていない —— 空セグメント(`nodes.**.` / `nodes.**..x`)や `**` 直後の素の `*`(`nodes.**.*`) | 宣言どおりに綴り、その後ろに実在するパスを書く |
|
|
2478
|
+
| `wcs/recursion-context` | **束縛**形の `**` を、束縛先の深さが無い場所で読んだ —— トップレベル、またはアンカー外の getter | 再帰 getter か行 getter の中から読むか、`[]` で全深さを合併する |
|
|
2479
|
+
| `wcs/recursion-getall-form` / `wcs/recursion-setall-form` | `**` に対して定義できない `indexes` の形。コードが付くのは非空の接頭辞(両 API)と、`$getAll` の配列でない `indexes`(`null`・文字列など)。`$setAll` の省略・mapper・`{ spread: true }` も同じ誤りで、lint は同じコードで報告するが、実行時は形を名指しした文面で throw するだけでコードは付かない | 現在の深さなら省略、全深さなら `[]` |
|
|
2480
|
+
| `wcs/recursion-structural-write` | 再帰 `$setAll` が構造そのもの(ノード・子リスト・その `length`・子ノード・反復サブパスが多段なら子リストへ至る途中のオブジェクト)を指している | 葉のプロパティへブロードキャストする |
|
|
2481
|
+
| `wcs/recursion-readonly` | 書き込みが再帰 getter、またはその導出値の内側を指している —— 再帰 `$setAll` の `nodes.**.total` でも、`nodes.*.children.*.total` のような具体的な展開形への書き込み(固定本数の `$setAll`・値付き `$resolve`・直接代入)でも | getter の導出元を書く |
|
|
2482
|
+
| `wcs/recursion-shared-list` / `wcs/recursion-cycle` | 走査が同じ配列インスタンスに 2 度到達した —— 2 つのノードが 1 本の子リストを共有、または自分の祖先から到達可能 | 各ノードに自分の子配列を持たせる |
|
|
2483
|
+
| `wcs/recursion-depth-exceeded` | 展開後のパスがワイルドカード 128 段を超える —— 木がエンジンのアドレス可能な深さより深いか、循環している | 木を平らにするか、循環を探す |
|
|
2484
|
+
|
|
2485
|
+
`wcs/recursion-*` の各行がどの形を拒否していて、代わりに何を書けばよいのかは、上の**再帰パス**の節に書いてあります。
|
|
2224
2486
|
|
|
2225
2487
|
`$resolve` / `$getAll` の**添字の超過は以前は黙って捨てられ**、取り違えたまま「もっともらしい値」が返っていました。現在はどちらもエラーです:
|
|
2226
2488
|
|
|
@@ -2312,6 +2574,21 @@ bootstrapState({
|
|
|
2312
2574
|
[docs/i18n-design.md](../../docs/i18n-design.md) を参照。短く言えば、翻訳はフィルタでは
|
|
2313
2575
|
なくパスに置く。
|
|
2314
2576
|
|
|
2577
|
+
**i18n の位置づけと、決めたこと。** i18n パッケージもライブな言語切替も、意図して
|
|
2578
|
+
持たない。辞書はロケールごとに選ばれる ES モジュールで、ボリューム
|
|
2579
|
+
(`<wcs-state mount="i18n" src="/i18n/state.js">`)としてマウントし、普通のパス
|
|
2580
|
+
(`i18n.checkout.title`)で読む。
|
|
2581
|
+
ロケールはページが描画される**前**に決める — フィルタは `<html lang>` から、router は
|
|
2582
|
+
URL から。router 側のロケールはルートパラメータではなく **basename**(`/ja/…`)に置く。
|
|
2583
|
+
したがって言語切替は state への書き込みではなく、別の basename への実ナビゲーションで
|
|
2584
|
+
ある: router は自分の basename 配下のリンクだけを intercept するので、`/:lang` パラメータ
|
|
2585
|
+
にすると言語が変わらないまま静かに壊れ、ライブ切替はロケール依存の全モジュールの
|
|
2586
|
+
再評価を要求する。basename を運ぶ `<base href>` には実コストがある(ページ内アンカー・
|
|
2587
|
+
SVG フラグメント参照・CSP 下の相対 `src` が全てそこを基準に解決される)。代替 2 案 —
|
|
2588
|
+
router 自身が `<html lang>` を読む案と、リンク単位の intercept オプトアウト — も検討し、
|
|
2589
|
+
記録として残した。別の形を選ぶ前に [docs/i18n-design.md](../../docs/i18n-design.md)
|
|
2590
|
+
§9-1 を読むこと。`examples/router-i18n` が参照レイアウト。
|
|
2591
|
+
|
|
2315
2592
|
> この 3 つは **architecture-hardening** 機能で、規範は `docs/architecture-hardening/` に
|
|
2316
2593
|
> あります。`enablePropagationContext` は**既定 on** — write-path コストは一方向バインドで
|
|
2317
2594
|
> ほぼゼロ(echo しうる双方向 wire のみ因果 bookkeeping を行う)で、フラグは恒久的な
|