@wcstack/state 3.2.0 → 3.4.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 +140 -83
- package/README.md +141 -84
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.d.ts +74 -8
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.d.ts +28 -5
- package/dist/manifest.esm.js +253 -246
- package/dist/parser.d.ts +16 -2
- package/dist/parser.esm.js +223 -50
- package/dist/split/chunks/binder.js +1 -1
- package/dist/split/chunks/binder.js.map +1 -1
- package/dist/split/chunks/checkDependency.js +1 -1
- package/dist/split/chunks/checkDependency.js.map +1 -1
- package/dist/split/chunks/declarationAliases.js +1 -1
- package/dist/split/chunks/declarationAliases.js.map +1 -1
- package/dist/split/chunks/declarationHooks.js +2 -0
- package/dist/split/chunks/declarationHooks.js.map +1 -0
- package/dist/split/chunks/diagnosticsHooks.js.map +1 -1
- package/dist/split/chunks/featureBridge.js +2 -0
- package/dist/split/chunks/featureBridge.js.map +1 -0
- package/dist/split/chunks/filterRegistry.js +1 -1
- package/dist/split/chunks/filterRegistry.js.map +1 -1
- package/dist/split/chunks/getAllPropertyDescriptors.js +1 -1
- package/dist/split/chunks/getAllPropertyDescriptors.js.map +1 -1
- package/dist/split/chunks/lifecycleHooks.js +1 -1
- package/dist/split/chunks/pathDiagnostics.js +1 -1
- package/dist/split/chunks/pathDiagnostics.js.map +1 -1
- package/dist/split/chunks/registerComponents.js +1 -1
- package/dist/split/chunks/registerComponents.js.map +1 -1
- package/dist/split/chunks/setAll.js +1 -1
- package/dist/split/chunks/setAll.js.map +1 -1
- package/dist/split/chunks/version.js +1 -1
- package/dist/split/core.d.ts +64 -2
- package/dist/split/core.js +1 -1
- package/dist/split/core.js.map +1 -1
- package/dist/split/features/devtools.js +1 -1
- package/dist/split/features/devtools.js.map +1 -1
- package/dist/split/features/diagnostics.js +1 -1
- package/dist/split/features/diagnostics.js.map +1 -1
- package/dist/split/features/formats.js +1 -1
- package/dist/split/features/formats.js.map +1 -1
- package/dist/split/features/recursion.js +1 -1
- package/dist/split/features/recursion.js.map +1 -1
- package/dist/split/features/scopes.js +1 -1
- package/dist/split/features/scopes.js.map +1 -1
- package/dist/split/features/ssr.js +1 -1
- package/dist/split/features/ssr.js.map +1 -1
- package/dist/split/features/temporal.js +1 -1
- package/dist/split/features/temporal.js.map +1 -1
- package/dist/wcs-manifest.json +36 -21
- package/package.json +1 -1
- package/dist/split/chunks/mount.js +0 -2
- package/dist/split/chunks/mount.js.map +0 -1
- package/dist/split/chunks/watchRuntime.js +0 -2
- package/dist/split/chunks/watchRuntime.js.map +0 -1
package/README.ja.md
CHANGED
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
|
|
89
89
|
- UIを作り直しても、状態のロジックに触れる必要がありません — ただしロジックが描画内容にぶら下がっていない範囲で。live binding は 3 つある[評価のきっかけ](#評価のきっかけdemand-root)の 1 つなので、表示専用のつもりの要素がページ唯一の購読になりえます。
|
|
90
90
|
- 状態のデータ構造をリファクタリングしても、パス文字列の更新だけで済みます。
|
|
91
|
-
- HTMLを読めば、すべてのバインディングを把握できます。HTMLに現れない依存(`$watch`・`$
|
|
91
|
+
- HTMLを読めば、すべてのバインディングを把握できます。HTMLに現れない依存(`$watch`・`$stream`・`$scan`)は、すべて state という 1 箇所に宣言されています。
|
|
92
92
|
|
|
93
93
|
このパスによる契約は、REST APIのURLと同じ発想です — 両者が合意するシンプルな文字列だけが存在し、そこに共有するコードはありません。これはJavaScriptの上に独自のテンプレート言語を発明するのではなく、HTML本来の宣言的な性質をフルに活かした結果として生まれた設計です。
|
|
94
94
|
|
|
@@ -132,12 +132,12 @@
|
|
|
132
132
|
| **集計と一括書き込み** | `$getAll` / `$setAll` / `$resolve` が `items.*.price` を横断して読み書きする。配列は作り直さない | [Proxy API](#proxy-api) |
|
|
133
133
|
| **再帰パス** | `$recursion` が木の形の繰り返し地点を宣言し、1 本の `**` getter が全深さを覆う | [再帰パス](#再帰パスrecursion) |
|
|
134
134
|
| **リアクティビティ** | ES Proxy がアドレス単位で読み取りを追跡・キャッシュし、依存順に無効化し、DOM 書き込みをマイクロタスクでまとめる | [状態の更新](#状態の更新) · [依存追跡の境界](#依存追跡の境界) |
|
|
135
|
-
| **getter が動く条件** | getter は遅延評価。評価需要は live binding・`$watch`・`$
|
|
135
|
+
| **getter が動く条件** | getter は遅延評価。評価需要は live binding・`$watch`・`$stream` の `args` の 3 箇所からしか生まれない | [評価のきっかけ](#評価のきっかけdemand-root) |
|
|
136
136
|
| **モジュール化** | `mount=` がモジュールを 1 本のツリーに接ぎ木し、`state: path` が部分木をコンポーネントにマウントする。個別対応付けは単一キーを繋ぎ、マウントされたコンポーネントの getter はマウント点で公開される | [ボリューム](#追加の状態をマウントするmount) · [丸ごとマウント](#丸ごとマウントstate-path) |
|
|
137
137
|
| **コンポーネント** | 排他的な 2 方式 — JavaScript クラス+`bind-component` か、HTML だけの DCC か | [機構の選び方](#コンポーネント機構の選び方) |
|
|
138
138
|
| **他要素との配線** | wc-bindable プロトコル、spread(`...: obj`)、`#init=` / `#sync=` の authority、プロパティ→属性ミラー | [バインディング authority](#バインディング-authority-init--sync) · [Spread](#spread-バインディング) · [Inputs](#inputs-と属性ミラー) |
|
|
139
139
|
| **トークン** | command token が state から要素のメソッドを呼び、event token が要素のイベントを state へ戻す | [Command token](#command-tokenメソッドバインディング) · [Event token](#event-tokenイベントバインディング) |
|
|
140
|
-
| **時間** | `$
|
|
140
|
+
| **時間** | `$stream` が非同期ソースを fold し、`$watch` が headless に反応し、`$scan` が両者を越えて残る累積値を持つ | [時間を扱う機構の選び方](#時間を扱う機構の選び方) |
|
|
141
141
|
| **初期化とライフサイクル** | state の供給は 6 通り。`$connectedCallback` 〜 `$stateReadyCallback`、`bootstrapState()` / `createState()` | [状態の初期化](#状態の初期化) · [ライフサイクルフック](#ライフサイクルフック) · [API リファレンス](#api-リファレンス) |
|
|
142
142
|
| **診断** | 解決しないパス・添字の本数・階数・getter の循環を報告する。失敗はそのバインディング 1 本に閉じ、値も DOM も巻き戻さない | [診断と失敗の扱い](#診断と失敗の扱い) |
|
|
143
143
|
| **配布** | ランタイム依存ゼロ、ビルド不要、ESM、CDN の `/auto` 1 タグ。`unsafe-eval` 不要で Trusted Types 対応 | [インストール](#インストール) · [docs/csp.ja.md](../../docs/csp.ja.md) |
|
|
@@ -186,15 +186,15 @@
|
|
|
186
186
|
<script type="importmap">
|
|
187
187
|
{
|
|
188
188
|
"imports": {
|
|
189
|
-
"@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.
|
|
190
|
-
"@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.
|
|
191
|
-
"@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.
|
|
189
|
+
"@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.4.0/dist/split/core.js",
|
|
190
|
+
"@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.4.0/dist/split/features/temporal.js",
|
|
191
|
+
"@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.4.0/dist/split/features/scopes.js"
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
</script>
|
|
195
195
|
<script type="module">
|
|
196
196
|
import { bootstrapState, installFeatures } from '@wcstack/state/core';
|
|
197
|
-
import temporal from '@wcstack/state/features/temporal'; // $watch / $scan / $
|
|
197
|
+
import temporal from '@wcstack/state/features/temporal'; // $watch / $scan / $stream
|
|
198
198
|
import scopes from '@wcstack/state/features/scopes'; // bind-component・mount=・DCC
|
|
199
199
|
|
|
200
200
|
installFeatures([temporal, scopes]);
|
|
@@ -202,6 +202,11 @@
|
|
|
202
202
|
</script>
|
|
203
203
|
```
|
|
204
204
|
|
|
205
|
+
`installFeatures([...])` は上の例のとおり `bootstrapState()` の**前**に呼びます。機能は自分のタグ
|
|
206
|
+
(`<wcs-ssr>`)を登録することがあり、タグを定義するのは bootstrap だからです。逆順でもエラーには
|
|
207
|
+
なりません — 後から install した機能は、すでに bootstrap 済みのレジストリへ自分のタグを定義して
|
|
208
|
+
追いつきます — が、宣言どおりの順で呼べば、最初の要素が upgrade する前にすべてのタグが揃います。
|
|
209
|
+
|
|
205
210
|
分割の形は、パッケージのファイルそのものから読みます — 上の例のように jsDelivr の素の `/npm/` パスで
|
|
206
211
|
版を固定するか(素のパスは `exports` を読まないので、`dist/split/` 以下のファイル名を書く)、バンドラを
|
|
207
212
|
通します。**`esm.run` からは読まないでください**。`+esm` はエントリごとにサーバー側で再バンドルし、
|
|
@@ -213,20 +218,23 @@
|
|
|
213
218
|
| エントリ | 足されるもの |
|
|
214
219
|
|---|---|
|
|
215
220
|
| `@wcstack/state/core` | バインディングの本体: `data-wcs`・`for` / `if`・パス getter・フィルタ・イベント・`$command` / `$on`・`bootstrapState`・`installFeatures` |
|
|
216
|
-
| `@wcstack/state/features/temporal` | `$watch`・`$scan`・`$
|
|
221
|
+
| `@wcstack/state/features/temporal` | `$watch`・`$scan`・`$stream` |
|
|
217
222
|
| `@wcstack/state/features/scopes` | `bind-component`・`mount=` のボリューム・オーバーレイの公開 getter・DCC(`data-wc-definition`) |
|
|
218
223
|
| `@wcstack/state/features/recursion` | `$recursion` と `**` パス |
|
|
219
224
|
| `@wcstack/state/features/ssr` | `enable-ssr`: サーバー描画とハイドレーション |
|
|
220
|
-
| `@wcstack/state/features/formats` | 書式フィルタ群(`
|
|
225
|
+
| `@wcstack/state/features/formats` | 書式フィルタ群(`upper`・`date`・`round`・`truncate` …)。core が答えるのは `if` / `else` が要る `not` だけ |
|
|
221
226
|
| `@wcstack/state/features/devtools` | DevTools Hook Protocol への source 登録 |
|
|
222
227
|
| `@wcstack/state/features/diagnostics` | 開発時の警告: 束縛・`$watch`・`$scan` のパスが state 上で解決できないとき、did-you-mean 付きで知らせる。入れなければ静か(本番向け)。throw するエラーの文言はどちらでも変わらない |
|
|
223
228
|
| `@wcstack/state/define` | `defineState` と型だけ — ランタイムは 0 |
|
|
224
229
|
|
|
225
230
|
機能が入っていない宣言は黙って無視されません。state の定義時に
|
|
226
231
|
`[wcs/feature-not-installed] … install it with installFeatures([...]) from "@wcstack/state/features/…"`
|
|
227
|
-
で落ち、実装の無いフィルタは束縛計画の段で `[wcs/filter-unknown]`
|
|
232
|
+
で落ち、実装の無いフィルタは束縛計画の段で `[wcs/filter-unknown]` で落ちます。`$watch` を宣言した
|
|
233
|
+
ボリューム(`<wcs-state mount=…>`)も接ぎ木の時点で同じ `[wcs/feature-not-installed]` で落ちます —
|
|
234
|
+
ボリュームの state は要素自身の宣言の門を通らないためです。install は冪等で、
|
|
228
235
|
どのエントリも core のチャンクを 1 つだけ共有します(機能側がエンジンの 2 つ目のコピーを抱える
|
|
229
|
-
|
|
236
|
+
ことも、**別の機能**のコピーを抱えることもありません — 機能をまたぐ呼び出しは、持ち主の機能が
|
|
237
|
+
install 時に埋める受け口を通ります)。
|
|
230
238
|
|
|
231
239
|
## 基本的な使い方
|
|
232
240
|
|
|
@@ -333,7 +341,7 @@
|
|
|
333
341
|
<div data-wcs="textContent: cart.total"></div>
|
|
334
342
|
```
|
|
335
343
|
|
|
336
|
-
ボリュームは getter・`$watch`・`$listKeys`・`$
|
|
344
|
+
ボリュームは getter・`$watch`・`$listKeys`・`$renderedCallback`・`$connectedCallback`/`$disconnectedCallback` を宣言できます(すべてマウントパス相対)。`$errorCallback` はルート専用です(バインディングの失敗はツリーの所有者へ 1 回だけ報告されます)。読み込み順は自由です(ルートより先に接続されたボリュームは、ルートの登録時に接ぎ木されます)。ルートの `<wcs-state>` が初期化に失敗した場合、その時点で待機していたボリュームは永久に待たずに自分の報告を出して決着します。その報告が終点です —— 孤児として報告されたボリュームは後から自分で接ぎ木し直さないので、あとから修正版のルートを接続しても復帰しません。接ぎ木しないまま決着したボリューム(孤児・ロード失敗・接ぎ木失敗)はマウントの枠を返します。ロード中やルート待ちのあいだに外れたボリュームも枠を返します。そうしたボリュームは、同じ root へ付け直したときはその場で、それ以外は接ぎ木の直前に枠を取り直し、枠が空いていれば従来どおり(外れたままでも)接ぎ木します。外れている間に別のボリュームが枠を取っていた場合は、それを報告して接ぎ木しません。ボリュームの `$connectedCallback` が同期で投げた場合も非同期の失敗と同じく報告に留まり、接ぎ木は済んだものとして扱います。ページを読み直さずに復旧するには、壊れたルートと孤児のボリュームを取り除いて新しい要素を追加してください。接ぎ木済みのボリュームは、外してもデータがツリーに残るので枠を握ったままです。マウントパスは静的パスのみです(`*`・`$`・`#`・`@` は不可)。初期化後に `mount` 属性を変更することはできません — 変更は console 警告付きで無視されます。要素を取り除き、望むパスで新しい要素を追加してください。
|
|
337
345
|
|
|
338
346
|
**ルートのパスをボリュームへ注入する(3.1)。** ボリュームのコードが自分の外のパスを読むには、ボリューム要素の `data-wcs` にコンポーネントの部分マウントと同じ形で書きます:
|
|
339
347
|
|
|
@@ -341,7 +349,7 @@
|
|
|
341
349
|
<wcs-state mount="cart" src="./cart.js" data-wcs="state.taxRate: settings.taxRate"></wcs-state>
|
|
342
350
|
```
|
|
343
351
|
|
|
344
|
-
`cart.js` の中(getter・メソッド・`$watch`・`$listKeys`・ライフサイクル)では `this.taxRate` がルートの `settings.taxRate` を読み書きし、依存も張られるので、`settings.taxRate` が変われば `get total()` も再評価されます。`$watch: { taxRate() {…} }` は `settings.taxRate` の変化で発火し、`$
|
|
352
|
+
`cart.js` の中(getter・メソッド・`$watch`・`$listKeys`・ライフサイクル)では `this.taxRate` がルートの `settings.taxRate` を読み書きし、依存も張られるので、`settings.taxRate` が変われば `get total()` も再評価されます。`$watch: { taxRate() {…} }` は `settings.taxRate` の変化で発火し、`$renderedCallback` はその更新を内側の名前 `taxRate` で受けます。注入はボリュームのコードの中の名前です — ページは今までどおり `settings.taxRate` を読み、`cart.taxRate` はツリーにありません。
|
|
345
353
|
|
|
346
354
|
- **1 キーずつ**: 左辺は `state.<キー>` の 1 段です(`state: …` でボリューム全体を動かすことはできません — `mount` を変えてください)。
|
|
347
355
|
- **注入が自前のキーに勝つ**: ボリュームが同名のデータキー(`taxRate: 0`)を宣言していても、その値は接ぎ木されません。同名の getter・setter・メソッドはどちらが `this.taxRate` か曖昧なので、そのボリュームは接ぎ木せずにエラーを報告します。
|
|
@@ -354,8 +362,8 @@
|
|
|
354
362
|
|---|---|---|---|
|
|
355
363
|
| データキー・getter・setter・メソッド | 動く | 動く(`p` 相対) | 動く — 自前のキーはホストがマップしない限り私有、getter は公開される |
|
|
356
364
|
| `$connectedCallback` / `$disconnectedCallback` | 動く | 動く(`p` 相対) | 動く |
|
|
357
|
-
| `$watch`・`$listKeys`・`$
|
|
358
|
-
| `$
|
|
365
|
+
| `$watch`・`$listKeys`・`$renderedCallback` | 動く | 動く(`p` 相対) | 動かない — `wcs/mount-dollar-declaration` を 1 回警告 |
|
|
366
|
+
| `$stream`・`$scan`・`$recursion`・`**` の getter | 動く | 接ぎ木の前にエラーで拒否 | 動かない — 警告 |
|
|
359
367
|
| `$commandTokens`・`$eventTokens`・`$on` | 動く | 動かない — 警告 | 動かない — 警告 |
|
|
360
368
|
| `$errorCallback` | 動く | 動かない — 警告(3.0 より前は無言) | 動かない — 警告(3.0 より前は無言) |
|
|
361
369
|
| 初期化の失敗 | 1 回報告し、`connectedCallbackPromise` を reject | 接ぎ木せずに決着して枠を返す。`connectedCallbackPromise` は resolve(`scopes` 機能が無いときは reject) | 1 回報告し、コンポーネントの `connectedCallbackPromise` を reject |
|
|
@@ -652,14 +660,16 @@ export default {
|
|
|
652
660
|
<wcs-fetch data-wcs="...: usersFetch; status: alternateStatus"></wcs-fetch>
|
|
653
661
|
```
|
|
654
662
|
|
|
655
|
-
**`undefined` は「無意見」** — 展開された state パスが `undefined` に解決される場合(slot オブジェクトでその input を初期化していない場合など)、プロパティ書き込みは**スキップ**され、要素側の既定値がそのまま生きます。実際に使うパスだけ初期化すれば十分で、`<wcs-fetch>` が `method` / `manual` / `body` を宣言していても `usersFetch: { value: null, loading: false }` だけで動きます。明示的にクリアしたい場合は `null` を代入してください(`null` は常に書き込まれます)。**表示の表面は別です(3.0):** `textContent` / `innerText` / `innerHTML`・mustache のテキスト・`attr.*`・`style.*` には生かすべき要素側の既定値が無いので、`undefined` も `null` も「値が無い」—
|
|
663
|
+
**`undefined` は「無意見」** — 展開された state パスが `undefined` に解決される場合(slot オブジェクトでその input を初期化していない場合など)、プロパティ書き込みは**スキップ**され、要素側の既定値がそのまま生きます。実際に使うパスだけ初期化すれば十分で、`<wcs-fetch>` が `method` / `manual` / `body` を宣言していても `usersFetch: { value: null, loading: false }` だけで動きます。明示的にクリアしたい場合は `null` を代入してください(`null` は常に書き込まれます)。**表示の表面は別です(3.0):** `textContent` / `innerText` / `innerHTML`・mustache のテキスト・`attr.*`・`style.*`・`class.*` には生かすべき要素側の既定値が無いので、`undefined` も `null` も「値が無い」— テキストは空になり、属性やスタイルは削除され、クラスは外れます。3.0 より前は `textContent:` も `undefined` をスキップしていたため使い回したリストの行に前の行の文字が残り、属性には文字列 `"undefined"` / `"null"` が入っていました。`class.*` だけは範囲が狭く、「値が無い」扱いになるのは `undefined` と `null` だけです — **それ以外の非 boolean は今も throw します**。クラスの束縛は真偽の切り替えなので、truthy な文字列は意図というより書き間違いである方がずっと多いためです。「truthy として扱う」つもりなら `class.on: flag|truthy` と書いてください。このスキップは spread に限らずすべてのプロパティバインディングに適用され、`config.debug` 時はスキップごとに `console.debug` でログが出ます。
|
|
664
|
+
|
|
665
|
+
**フィルタを挟んでも契約は保たれます。** 空値の判定はフィルタ**後**の値に対して行われるため、`String(value)` で結果を組み立てるフィルタは値が無いときに `"undefined"` という**文字**を作ってしまい、`attr.title: x|trim` が `title="undefined"` を書いていました(フィルタの無い `attr.title: x` は属性を消します)。書式の族(`upper` / `lower` / `capitalize` / `trim` / `slice` / `substr` / `padStart` / `padEnd` / `repeat` / `reverse` / `truncate` / `unit`)は `undefined` / `null` を素通しするようになり、適用側の「テキストは空・属性は削除」がそのまま効きます。次の 3 群は**意図的に素通ししません**: 空値そのものが入力として意味を持つフィルタ(`defaults` / `coalesce` / `nullIfEmpty` / `boolean` / `truthy` / `falsy` / `not` / `eq` / `ne`)、変換が仕事のフィルタ(`int` / `float` / `number` / `string` — `undefined|string` は今も `"undefined"`)、値の型を検査するフィルタ(数値・日付・配列の族。`toFixed` / `date` / `join` など)。最後の群は throw しますが、それは束縛 1 本に閉じ込められ `$errorCallback` に報告されます。値が無いことが正当にありうるなら前に `coalesce` を置いてください(`price|coalesce(0)|toFixed(2)`)。
|
|
656
666
|
|
|
657
667
|
**制約事項**:
|
|
658
668
|
|
|
659
669
|
- spread 右辺へのフィルタ(`...: target|filter`)はエラー
|
|
660
670
|
- 右辺パスの途中に `*` を含めても OK(例:`...: stores.*.fetch`)
|
|
661
671
|
- 右辺は素のツリーパス(`...: fetchX`、途中の `*` も可)
|
|
662
|
-
- カスタム要素クラスが未登録の場合、`customElements.whenDefined(tag)` 解決時に遅延展開される(autoloader
|
|
672
|
+
- カスタム要素クラスが未登録の場合、`customElements.whenDefined(tag)` 解決時に遅延展開される(autoloader による遅延ロードに対応)。`for:` / `if:` のテンプレートの中でも同じ — 行や枝はすぐに描かれ、クラスが定義されたらそれぞれの行の要素をその行の値で展開する。定義前に消えた行・閉じた枝は展開しない。待つ間に文書から外した要素(タブやパネルの DOM 退避)は、外れている間はレジストリに何も残さず、戻ったら戻った先のツリーのレジストリで待ち直し、クラスが定義されたら(定義済みならすぐに)展開する(行の要素は、その行がまだ一覧に居る間だけ)
|
|
663
673
|
- `wcBindable` 宣言**のない**要素はエラー(明示配線で書いてください)。spread は何を展開すべきかを契約から読み取るため
|
|
664
674
|
|
|
665
675
|
**Composite shell**(wc-bindable Composition Profile)はそのままサポートされます:composite shell は標準の `target.constructor.wcBindable` を通じて synthesized declaration を露出するため、`"s3.progress"` のような composed name はフラットな要素メンバーキーとして扱われます。state を composed 構造に合わせて (`{ s3: { progress: 0 } }`) 持てば、`...: pipeline` が自動的に nested state path へ展開されます。
|
|
@@ -726,7 +736,7 @@ this.items = await (await fetch("/api/items")).json();
|
|
|
726
736
|
|---|---|---|
|
|
727
737
|
| `.name` | `users.*.name` | 現在の要素のプロパティ |
|
|
728
738
|
| `.` | `users.*` | 現在の要素そのもの |
|
|
729
|
-
| `.name\|
|
|
739
|
+
| `.name\|upper` | `users.*.name\|upper` | フィルタは保持される |
|
|
730
740
|
|
|
731
741
|
プリミティブ配列では、`.` が要素の値を直接参照します:
|
|
732
742
|
|
|
@@ -1042,7 +1052,7 @@ export default {
|
|
|
1042
1052
|
|
|
1043
1053
|
3. **キャッシュ** — getter の結果は具体的なアドレス(パス + ループインデックス)ごとにキャッシュされます。`users.*.fullName` のインデックス 0 とインデックス 1 は別々のキャッシュエントリを持ちます。依存先が変更された場合のみキャッシュが無効化されます。
|
|
1044
1054
|
|
|
1045
|
-
4. **直接インデックスアクセス** — 数値インデックスで特定の要素にアクセスすることもできます:`this["users.0.name"]` はループコンテキストなしで `users[0].name`
|
|
1055
|
+
4. **直接インデックスアクセス** — 数値インデックスで特定の要素にアクセスすることもできます:`this["users.0.name"]` はループコンテキストなしで `users[0].name` に解決されます。要素のパスへ別のオブジェクトを代入する(`this["users.0"] = { ...this["users.0"], name: "z" }`)と、`for:` でリストを描いているかどうかに関わらず、その位置は新しい行になります。その下のパス(`users.0.name`・`$getAll("users.*.name")`・行 getter)は、差し替える前の行のキャッシュではなく新しいオブジェクトから読みます。その場で書き換えた同じオブジェクトを書き戻す(`const u = this["users.0"]; u.name = "z"; this["users.0"] = u;`)か、`$postUpdate("users.0")` / `$postUpdate("users")` で知らせると、行は据え置いたまま、その下のパスとそれを読む getter を読み直します — 行の中でそれらを描く `for:` が無くても同じです。この修正の前は、`for:` が描いていない限りその下のパスはキャッシュの値のままで、同じリストを `$getAll` で集める getter が先に評価されていると、`users.0.name` を読む getter が要素の差し替えを取りこぼすことがありました。数値の添字が 1 つのパスなら、バインディングにも同じ綴りを書けて、意味も同じ「*いま 0 番目にある行*」です:`textContent: users.0.name`、`{{ users.1.name }}`、行 getter なら `{{ users.0.fullName }}`。添字のパスへの書き込み(`this["users.0.name"] = …`)、その行を描く `for:` の行やその行に束縛した入力欄からの書き込み、要素の差し替え(`this["users.0"] = {…}`)、並べ替え・行の削除・リストの丸ごと置換に追従し、行 getter はその入力の変化に追従します — リストを `for:` で描いていてもいなくても同じです。このバインディングは state オブジェクトに足す列挙されないアクセサを通して getter と同じように読まれ、getter と同じく、リストのどの行への書き込みでも評価し直されます。そのため `$watch` のキー(`"users.0.name"` など)は値が変わらないまま発火することがあります。その位置に行が無い(空のリスト)ときは空で、配列でないオブジェクトの下のキー(`sales.2024.total`)は素のキーとして読みます。3.3.0 までは、この書き方のバインディングは最初の値のまま止まってリストの丸ごと置換にしか追従せず、行 getter をこの綴りで書くと空のまま `wcs/binding-path-missing` の警告が出ていました。数値の添字が 2 つ以上のパス(`groups.0.items.1.v`)と、拡張できない state オブジェクト(`Object.freeze`・`Object.seal`・`Object.preventExtensions`)の上の数値添字のバインディングは、いまもそのままです。
|
|
1046
1056
|
|
|
1047
1057
|
### getter は state に対して純粋であること
|
|
1048
1058
|
|
|
@@ -1059,9 +1069,9 @@ get total() { return this.price * exchangeRate; } // モジュール変
|
|
|
1059
1069
|
|
|
1060
1070
|
| API | 用途 |
|
|
1061
1071
|
|---|---|
|
|
1062
|
-
| `this.$
|
|
1072
|
+
| `this.$dependOn(path)` | 依存を明示的に追加し、そのパスの変更でこの getter を dirty にする |
|
|
1063
1073
|
| `this.$postUpdate(path)` | 追跡外の入力が変わったことを getter の外から通知する |
|
|
1064
|
-
| `this.$
|
|
1074
|
+
| `this.$untracked(fn)` | 依存として登録せずにパスを読む(上の対称) |
|
|
1065
1075
|
| `this.$eq(path, key)` / `$eqPath(path, keyPath)` / `$eqIndex(path)` | 鍵付き購読: 「`path` はこの行の鍵に等しいか」をパターン依存なしで答える([鍵付き選択](#鍵付き選択eq--eqpath--eqindex)) |
|
|
1066
1076
|
|
|
1067
1077
|
```javascript
|
|
@@ -1085,9 +1095,9 @@ getter の例外は握り潰されません。評価された場所(バイン
|
|
|
1085
1095
|
| **setter の中の読み取りは追跡しない。** setter は命令的な代入であって派生ではないので、その中で読んだものは何の依存にもならない | 何を書くかを `this.a` を読んで決める setter は、`a` が変わっても再実行されない。再実行されるのは getter だけ |
|
|
1086
1096
|
| **同値ガードはプリミティブにだけ効く。** 現在値と `Object.is` で等しいプリミティブの書き込みはキューに入る前に落とされる。オブジェクト・配列の書き込みは同じ参照でも必ず通る | 同じ文字列を再代入しても何も起きない。同じオブジェクトを再代入するとバインディングと `$watch` が再発火する(`config.sameValueGuard`。`semantics: "event"` のプロパティはどちらにせよ対象外) |
|
|
1087
1097
|
|
|
1088
|
-
規則 1 は静的解析で捕まえられる唯一の規則です。getter が `this.form.name` を読んでいて、ドキュメントのどこかで `form.name` を書いている(`value:` バインド・spread・`this["form.name"] = …`)と、`wcs-validate` と VS Code 拡張が `wcs/getter-untracked-read` を報告します。ルートを丸ごと置換するだけの設計(router の params・`$
|
|
1098
|
+
規則 1 は静的解析で捕まえられる唯一の規則です。getter が `this.form.name` を読んでいて、ドキュメントのどこかで `form.name` を書いている(`value:` バインド・spread・`this["form.name"] = …`)と、`wcs-validate` と VS Code 拡張が `wcs/getter-untracked-read` を報告します。ルートを丸ごと置換するだけの設計(router の params・`$stream` の fold)には出ません。
|
|
1089
1099
|
|
|
1090
|
-
`$
|
|
1100
|
+
`$untracked(fn)` は setter の規則を getter に意図的に適用するもので、`fn` の中の読み取りは追跡されません。`$dependOn(path)` は最初の規則に対する逃げ道です。
|
|
1091
1101
|
|
|
1092
1102
|
### 鍵付き選択(`$eq` / `$eqPath` / `$eqIndex`)
|
|
1093
1103
|
|
|
@@ -1119,7 +1129,9 @@ export default {
|
|
|
1119
1129
|
- **`path` への書き込み** — オブジェクトを含むどんな値でも(`$eq("selected", this["items.*"])` に対する `this.selected = row`): 選ばれていた行と、新たに選ばれる行。
|
|
1120
1130
|
- **`path` より上のオブジェクトへの書き込み** — `$eq("sel.id", …)` に対する `this.sel = { id: 2 }`: 同じく 2 行。旧いオブジェクトの下と新しいオブジェクトの下で `path` が持つ値を鍵にします。
|
|
1121
1131
|
- **`path` が getter か getter の配下**(`get current()` に対する `$eq("current.id", …)`): 値が書き込みなしに変わるので、依存を張る普通の読み取りに戻ります。選択は正しく保たれますが、変化のたびに全行が再評価されます(鍵付きの形を使わないのと同じ)。2 行で済ませるには、`path` を書き込まれる state(`selectedId`)に向けてください。`@wcstack/devtools` の State ペインは、path ごとの購読の数を **Keyed selection** に出し、この形に落ちた path には `tracked` バッジを付けます(3.0)。
|
|
1132
|
+
- **数値の添字を含む `path`**(`$eq("items.0.v", …)`): 書き込みは添字をワイルドカードにした綴り(`items.*.v`・`items.*`)で知らせるので、綴りのままの購読には届きません。この形も依存を張る普通の読み取りに戻り、その行の `v` への書き込み・要素の差し替え・リストの差し替えに追従します(getter と同じく、別の行の `v` への書き込みでも再評価されます)。この修正の前は、同じ数値のパスのバインディングが無い限り最初の値のまま止まっていました。
|
|
1122
1133
|
- **型変換はしません:** `"2"` は id `2` に一致しません。`<input>` や `<select>` は文字列を書くので、入口で変換する(`value|number: selectedId`)か、id を文字列で持ってください。
|
|
1134
|
+
- **スコープの中では `path` は相対**です — パスを取る他の `$` API と同じです。マウントされたコンポーネント(`bind-component`)とボリューム(`<wcs-state mount="cart">`)では、`$eq("selectedId", …)`・`$eqPath` の 2 つのパス・`$eqIndex`・`$dependOn` はどれもそのスコープ(`cart.selectedId`)に解決され、ルートには向きません。`$untracked` はコールバックを取るので翻訳の対象外です。
|
|
1123
1135
|
|
|
1124
1136
|
### ループインデックス変数(`$1`, `$2`, ...)
|
|
1125
1137
|
|
|
@@ -1151,7 +1163,7 @@ export default {
|
|
|
1151
1163
|
|
|
1152
1164
|
```html
|
|
1153
1165
|
<template data-wcs="for: items">
|
|
1154
|
-
<td>{{ $1|
|
|
1166
|
+
<td>{{ $1|add(1) }}</td> <!-- 1始まりの行番号 -->
|
|
1155
1167
|
</template>
|
|
1156
1168
|
```
|
|
1157
1169
|
|
|
@@ -1266,6 +1278,12 @@ export default {
|
|
|
1266
1278
|
|
|
1267
1279
|
この書き方で行を入れ替えると、入れ替えが揃った時点で、描画済みの行が値と一緒に並べ替わります(その場で中身を書き換えるのではありません)。行の `$1` と、バインドの外にある行の状態(バインドしていない input に入力中のテキストなど)も値に付いてきます。リストに無かった値を書き込むと、その行はその場で置き換わります。ブロックは動かず、バインドが新しい値を映すので、行にバインドした input は入力中もフォーカスを保ちます。プリミティブのリストでは等しい値を区別できないため、書き込みの結果が同じ値の並べ替えになれば入れ替えとして扱います。
|
|
1268
1280
|
|
|
1281
|
+
入れ替えが揃うのは、動かした値がどれも元の位置を離れたときです。それまで(2 回の書き込みの間、動かした値が両方の位置にある間)は、行はブロックを保ったまま位置どおりの値を映し、添字のパス(`this["items.0"]`)はその位置を読み書きします。2 回の書き込みは別々の更新(2 回のクリックなど)に分かれていても構いません。入れ替えを揃えた更新が `items.*` の `$watch` に知らせるのは、その更新で書いた位置だけです(`prev` もその位置の値です)。揃う前に配列を代入すると、前にそのパスが持っていた配列でも、代入した配列の値が描かれます。別々の行に同じ値を持つリストもほかと同じく入れ替わりますが、別の行が持ち続ける値を写す書き込みは、入れ替えの片側と区別できません。そのため、コードが同じ値を持ち続ける行を作ると、配列を置き換えるまで、その配列への添字の書き込みは位置どおりに描かれます。値と `$1` は正しく、ブロックが値に付いて動かなくなるだけです。
|
|
1282
|
+
|
|
1283
|
+
行の中の要素が自分の行へ書く値には、ここまでの話は当てはまりません(`value: .` でバインドした input、`status: .` でバインドした wc-bindable の出力)。その書き込みは行の値の更新で、入れ替えの一部には数えません。行はブロックと `$1` を保ち、いくつもの行が同じ値を返しても、ある行の要素が別の行の持つ値を書いても、その要素の以後の書き込みは自分の行に届きます。
|
|
1284
|
+
|
|
1285
|
+
**同じオブジェクトが 2 つの位置にあるとき。** リストの 2 つの位置に同じオブジェクトがあると(`items: [o, o, …]`)、片方の行の下のキーへの書き込み(`this["items.0.name"] = "z"`)でオブジェクトは変わりますが、もう片方の行は古い値を表示し、読みも古い値を返します。行の下の値は行ごとにキャッシュされ、書き込みはそれを通した行にしか届かないためです。こうした書き込みの後に `$postUpdate("items")` を呼ぶと、`for:` の行・`$getAll("items.*.name")`・`items.1.name` のような添字のパスのすべてがオブジェクトを読み直します。
|
|
1286
|
+
|
|
1269
1287
|
## 再帰パス(`$recursion`)
|
|
1270
1288
|
|
|
1271
1289
|
パスは深さを文字列に焼き付けます。`nodes.*.children.*.total` はワイルドカードちょうど 2 段のパスであり、木が 1 段深くなっても 3 段には伸びません。しかし木の深さはコードではなく**データの性質**です。`$recursion` はこの隔たりを埋めます。形が繰り返す場所を宣言し、あとは「いま何段目であれ」を `**` と書きます。
|
|
@@ -1337,7 +1355,7 @@ k=2 nodes.*.children.*.children.*
|
|
|
1337
1355
|
| `$getAll(path, [])`(**明示**) | **全深さの合併** —— 深さ優先・行きがけ・添字昇順 |
|
|
1338
1356
|
| `$getAll(path, [i, …])` | 拒否。接頭辞ではどの深さの話か言えない(`wcs/recursion-getall-form`) |
|
|
1339
1357
|
| `$setAll(path, [], value)` | 全深さへのブロードキャスト(合併と同じ走査・同じ順序) |
|
|
1340
|
-
| `$resolve` / `$postUpdate` / `$
|
|
1358
|
+
| `$resolve` / `$postUpdate` / `$dependOn` / `$watch` のキー / `$listKeys` のキー / markup の `data-wcs` / 直接代入 | 拒否(`wcs/recursion-unsupported`) |
|
|
1341
1359
|
|
|
1342
1360
|
束縛形は束縛先の深さを必要とするので、**再帰 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 でもメソッドでも読めます。
|
|
1343
1361
|
|
|
@@ -1406,8 +1424,8 @@ this.$setAll("nodes.**.selected", [], false); // 全深さの全ノード
|
|
|
1406
1424
|
| 添字の省略 | 書き込み API は文脈を取らないので束縛する深さが無い —— `[]` を渡す |
|
|
1407
1425
|
| mapper 関数 | `(current, ...indexes)` の添字の本数が深さごとに変わる |
|
|
1408
1426
|
| `{ spread: true }` | 1 次元配列を木へ配るには作者が走査順を知っている必要があり、契約として使えない |
|
|
1409
|
-
| `nodes.**` / `nodes.**.children` / `nodes.**.children.*` / `nodes.**.children.length` —— 反復サブパスが多段(`branch.children.*`)なら、子リストへ至る途中の `nodes.**.branch`
|
|
1410
|
-
| `nodes.**.total`、およびその値の内側を指すパス | 再帰 getter に setter は無い。導出元を書く(`wcs/recursion-readonly`) |
|
|
1427
|
+
| `nodes.**` / `nodes.**.children` / `nodes.**.children.*` / `nodes.**.children.length` —— 反復サブパスが多段(`branch.children.*`)なら、子リストへ至る途中の `nodes.**.branch` も。添字綴りも同じ形に畳まれるので `nodes.**.children.0` は子ノード | 構造そのものへの書き込み(`length` への代入はリストを切り詰める)は、その書き込み自身が確定済みの子アドレスを壊す(`wcs/recursion-structural-write`) |
|
|
1428
|
+
| `nodes.**.total`、およびその値の内側を指すパス —— 添字綴りも同じ形に畳まれるので `nodes.**.children.0.total` も getter | 再帰 getter に setter は無い。導出元を書く(`wcs/recursion-readonly`) |
|
|
1411
1429
|
|
|
1412
1430
|
読み取り専用の規則は `**` の綴りに依存しません。再帰 getter の具体的な展開形 —— `nodes.*.total` / `nodes.*.children.*.total` / … —— への書き込みも、固定本数の `$setAll` でも値付きの `$resolve(path, indexes, value)` でも直接代入でも、またその深さが実体化済みかどうかに関わらず、書き込みの入口で拒否します。この検査が無かったときは、未実体化の展開形が「無いキー」に見えてノードのオブジェクトに書き込まれ、代入値が getter のキャッシュ結果として固定されていました。
|
|
1413
1431
|
|
|
@@ -1415,7 +1433,9 @@ this.$setAll("nodes.**.selected", [], false); // 全深さの全ノード
|
|
|
1415
1433
|
|
|
1416
1434
|
走査は深さ方向に降りながら、必要な形をその場で検査します。**同じ配列インスタンス**に 2 度到達したら拒否します。その配列が現在のノードの祖先のものなら循環(`wcs/recursion-cycle`)、そうでなければ 2 つのノードが 1 本の子リストを共有しています(`wcs/recursion-shared-list`)。各ノードに自分の `children` 配列を持たせてください —— **空**配列の使い回しは行を持たず別名化のしようがないので、追跡もせず正当です。
|
|
1417
1435
|
|
|
1418
|
-
行オブジェクトを作り直して `children` 配列を引き継ぐ置換 —— `this.nodes = this.nodes.map(n => ({ ...n }))` —— はふつうの更新で、集計もそのまま追従します。子リストの行オブジェクトはそのまま生き残り(行の identity で持っているもの —— `bind-component` の子スコープが描画した行や、そこにバインドしていない状態 —— は保たれます)、行がぶら下がっていた**退役した親**だけが生きている行に差し替わるので、次の葉の更新は画面に出ている行を dirty にします([#256](https://github.com/wcstack/wcstack/issues/256))。2 つの行が 1 本の `children` 配列を**共有**する形はこれとは別です。2 行とも配列に居る間は従来どおりで、1 本の配列には 1 組の行しかないので 2 行は必ず同じ値で一致し、親を読む行 getter(`this["nodes.*.value"]
|
|
1436
|
+
行オブジェクトを作り直して `children` 配列を引き継ぐ置換 —— `this.nodes = this.nodes.map(n => ({ ...n }))` —— はふつうの更新で、集計もそのまま追従します。子リストの行オブジェクトはそのまま生き残り(行の identity で持っているもの —— `bind-component` の子スコープが描画した行や、そこにバインドしていない状態 —— は保たれます)、行がぶら下がっていた**退役した親**だけが生きている行に差し替わるので、次の葉の更新は画面に出ている行を dirty にします([#256](https://github.com/wcstack/wcstack/issues/256))。2 つの行が 1 本の `children` 配列を**共有**する形はこれとは別です。2 行とも配列に居る間は従来どおりで、1 本の配列には 1 組の行しかないので 2 行は必ず同じ値で一致し、親を読む行 getter(`this["nodes.*.value"]`)はその行集合の**持ち主**(最初にその配列を展開した行)の文脈で評価されます。変わったのは**持ち主をリストから外したとき**で、行集合はリストに残っている行のどれか 1 行へ移ります(`if:` で隠れている行のこともあります) —— その行の集計が、外した行の数字で凍る代わりに共有データを追従します(行集合は 1 組しかないので、3 行で共有していれば残った 1 行だけが追従し、他は凍ったままです)。3.4.0 からは、持ち主がリストに残ったまま配列を手放したとき(`this["nodes.0.children"] = [ … ]`)も同じで、行集合は同じキーでその配列を持つ別の行へ移り、その行の集計が共有データを追従します(3.3.0 では凍ったままでした)。外した行**そのもののオブジェクト**が戻ってくれば持ち主も戻ります —— 同じ配列インスタンスでも、同じ行を並べた新しい配列でも、違う位置に戻しても返ります。一方、**全ての行を作り直す**綴り(`this.nodes = this.nodes.map(n => ({ ...n }))` —— この項の冒頭の更新)ではどの行も一致しないので、行集合は**持ち主が居た位置を占める行**に付きます。2.3.0 でこの通りに戻るのは同じ配列インスタンスの綴りだけで、新しい配列で戻すと 2.3.0 では両方の行が凍ります。子の getter が上を読むなら、ノードごとに自分の配列を持たせてください —— 持ち主が配列を手放した後、そうした getter は、読む値が変わるまで前の持ち主の文脈で求めた値を返し続けることがあります。
|
|
1437
|
+
|
|
1438
|
+
共有の形のうち次の 3 つは既知の制約です。別の外側の行が同じ配列を**別のキー**に持つと(ある行は `items` に、別の行は `alt` に)、片方のキーを通した書き込みが別の要素に着地することがあります([#396](https://github.com/wcstack/wcstack/issues/396))。同じ配列を 2 つの `<wcs-state>`(ルートとそのボリュームを含む)で共有しないでください —— 片方で写しや作り直しをした後、もう片方を通した書き込みが別の配列に着地したり、投げて失われたりします([#397](https://github.com/wcstack/wcstack/issues/397))。また、共有した配列の写し・移動・作り直しを 1 回の更新に重ねると、state は正しいのに一部の `for:` の描画が古いまま残ることがあります([#398](https://github.com/wcstack/wcstack/issues/398))。
|
|
1419
1439
|
|
|
1420
1440
|
上限は展開後のパスの**ワイルドカード 128 段**です。上の集計 getter は評価中のノードより 1 段下を読むので、127 段の鎖までは畳めて、128 段で `wcs/recursion-depth-exceeded` になります(アンカー・到達した深さ・組み立てようとしたパス・上限を名指しします)。この検査は getter 評価スタック自身の 128 段の上限(`wcs/getter-depth-exceeded`)より先に効くので、深い木は「深い」と報告され、循環の疑いを掛けられることはありません。途中で打ち切ることもしません —— 部分的な集計は、誤った値を正しい値として返すことだからです。
|
|
1421
1441
|
|
|
@@ -1463,7 +1483,7 @@ customElements.define("tree-node", class extends HTMLElement {
|
|
|
1463
1483
|
- 再帰 setter、接尾辞が構造そのものを名指す `**` getter(`get "nodes.**.children"()`)、`**` getter の展開形と同名の具体 getter、そして代入による `**` 経由の書き込み(`this["nodes.**.x"] = v`、`++` も含む)
|
|
1464
1484
|
- 再帰 `$setAll` の mapper・`{ spread: true }`・添字省略・非空の接頭辞・配列でない `indexes`。書き込み API には深さを束縛する評価文脈が無いので、`[]` は必須です
|
|
1465
1485
|
- 再帰 `$getAll` の非空の接頭辞・配列でない `indexes`。**添字省略は正当です** —— 再帰 getter の中では束縛形で、評価中の深さを読みます
|
|
1466
|
-
- `data-wcs` / `$watch` や `$listKeys` のキー / `$resolve` / `$postUpdate` / `$
|
|
1486
|
+
- `data-wcs` / `$watch` や `$listKeys` のキー / `$resolve` / `$postUpdate` / `$dependOn` への `**`
|
|
1467
1487
|
- ボリューム(`mount=`)やマウントされたコンポーネント(`bind-component`)の `$recursion` と `**` getter —— ルートの state に置きます
|
|
1468
1488
|
- 再帰 `<template>`、`$depth` 変数、公開の `maxDepth` オプション(3 つとも存在しません)
|
|
1469
1489
|
|
|
@@ -1509,7 +1529,7 @@ export default {
|
|
|
1509
1529
|
|---|---|---|
|
|
1510
1530
|
| `eq(value)` | 等しい | `count\|eq(0)` → `true/false` |
|
|
1511
1531
|
| `ne(value)` | 等しくない | `count\|ne(0)` |
|
|
1512
|
-
| `not` |
|
|
1532
|
+
| `not` | 真偽性を反転 — `0` / `""` / `null` / `undefined` はすべて `true`(3.x。以前は非 boolean で throw し、`else:` がどちらの枝も描かなかった) | `isActive\|not` |
|
|
1513
1533
|
| `lt(n)` | より小さい | `count\|lt(10)` |
|
|
1514
1534
|
| `le(n)` | 以下 | `count\|le(10)` |
|
|
1515
1535
|
| `gt(n)` | より大きい | `count\|gt(0)` |
|
|
@@ -1548,12 +1568,12 @@ export default {
|
|
|
1548
1568
|
| `capitalize`(`cap`) | 先頭大文字 | `name\|capitalize` |
|
|
1549
1569
|
| `trim` | 空白除去 | `text\|trim` |
|
|
1550
1570
|
| `slice(n)` | 文字列スライス | `text\|slice(5)` |
|
|
1551
|
-
| `substr(start, length)` |
|
|
1571
|
+
| `substr(start, length)` | 部分文字列(引数は 2 つとも必須) | `text\|substr(0,10)` |
|
|
1552
1572
|
| `padStart(n, char?)`(`pad`) | 先頭を埋める(既定 `0`) | `id\|padStart(5,0)` → `"00001"` |
|
|
1553
1573
|
| `padEnd(n, char?)` | 末尾を埋める(既定は空白。3.2) | `code\|padEnd(8)` |
|
|
1554
1574
|
| `repeat(n)`(`rep`) | 繰り返し | `text\|repeat(3)` |
|
|
1555
1575
|
| `reverse`(`rev`) | 反転 | `text\|reverse` |
|
|
1556
|
-
| `truncate(n, suffix?)` |
|
|
1576
|
+
| `truncate(n, suffix?)` | 切り詰めて省略記号を付加(既定は `…` — U+2026 の 1 文字) | `title\|truncate(20)` → `"…"` 付き |
|
|
1557
1577
|
| `join(sep?)` | 配列を連結(既定 `", "`) | `tags\|join` / `tags\|join(/)` |
|
|
1558
1578
|
|
|
1559
1579
|
### 型変換
|
|
@@ -1571,11 +1591,13 @@ export default {
|
|
|
1571
1591
|
|
|
1572
1592
|
| フィルタ | 説明 | 例 |
|
|
1573
1593
|
|---|---|---|
|
|
1574
|
-
| `date(loc?)` | 日付フォーマット | `
|
|
1575
|
-
| `time(loc?)` | 時刻フォーマット | `
|
|
1576
|
-
| `datetime(loc?)` | 日付 + 時刻 | `
|
|
1577
|
-
| `ymd(sep?)` | YYYY-MM-DD | `
|
|
1578
|
-
| `hms(sep?)` | HH:MM:SS | `
|
|
1594
|
+
| `date(loc?)` | 日付フォーマット | `createdAt\|date` / `createdAt\|date(ja-JP)` |
|
|
1595
|
+
| `time(loc?)` | 時刻フォーマット | `createdAt\|time` |
|
|
1596
|
+
| `datetime(loc?)` | 日付 + 時刻 | `createdAt\|datetime(en-US)` |
|
|
1597
|
+
| `ymd(sep?)` | YYYY-MM-DD(既定は `-`) | `createdAt\|ymd` / `createdAt\|ymd(/)` |
|
|
1598
|
+
| `hms(sep?)` | HH:MM:SS(既定は `:`) | `createdAt\|hms` / `createdAt\|hms(-)` |
|
|
1599
|
+
|
|
1600
|
+
**5 つとも `Date` を取ります**(タイムスタンプではありません)。他の型は「requires a date value」で拒否されます。JSON から読み込んだ state は数値か ISO 文字列を持つので、束縛ではなく読み出し側で変換してください: `get createdAt() { return new Date(this.createdAtMs); }`。`loc?` の既定は `config.locale` で、**適用のたびに**読み直されるため、束縛を組み立てた後に確定したロケールも反映されます(明示引数 `date(ja-JP)` は束縛式の一部なので組み立て時に固定されます)。
|
|
1579
1601
|
|
|
1580
1602
|
### 真偽値 / デフォルト
|
|
1581
1603
|
|
|
@@ -1596,7 +1618,15 @@ export default {
|
|
|
1596
1618
|
|
|
1597
1619
|
フィルタは束縛計画の段で解決されます。未知の名前は `[wcs/filter-unknown]`(did-you-mean 付き)で、3.0 からは受け付ける個数の外の引数も `[wcs/filter-arity]`(`join(a,b)`: "accepts at most 1 argument(s) (2 given)")で落ちます — lint と同じコード・同じ範囲です。引数は構造のままキャッシュされるので、`join('a,b')` と `join(a)` を取り違えません。
|
|
1598
1620
|
|
|
1599
|
-
**引数のリテラルは型を持ちます(3.0)。** 引用符の無い `true` / `false` / `null` / 数値はその値、引用符付きの引数は文字列です。比較フィルタは真偽値と `null` の比較にこれを使います: `done|eq(true)` は `true` に一致し(3.0 より前は文字列 `"true"` と比べていて一致しなかった)、`eq('true')` は一致せず、`eq(null)` は `null` に一致します。数値と文字列の比べ方は変わりません — 数値の値は数と、文字列の値は原文と比べるので、フォームの値 `"1"` は今も `eq(1)`
|
|
1621
|
+
**引数のリテラルは型を持ちます(3.0)。** 引用符の無い `true` / `false` / `null` / 数値はその値、引用符付きの引数は文字列です。比較フィルタは真偽値と `null` の比較にこれを使います: `done|eq(true)` は `true` に一致し(3.0 より前は文字列 `"true"` と比べていて一致しなかった)、`eq('true')` は一致せず、`eq(null)` は `null` に一致します。数値と文字列の比べ方は変わりません — 数値の値は数と、文字列の値は原文と比べるので、フォームの値 `"1"` は今も `eq(1)` に一致します。**数値**の値と型付きの `true` / `false` / `null` は単に「一致しない」です: `selectedId|eq(null)` は id が `null` の間は `true`、数値になれば `false` になります(3.2 まではこの組み合わせが**適用のたびに** `requires a number as option` で throw していたため、null と数値の両方を取りうるパスは値が数値になった瞬間にその束縛が壊れていました)。型の食い違いとして報告されるのは、引用符の無い非数値(数値の値に対する `eq(abc)`)だけです — `eq` は値の型を選ばないので、`status|eq(active)` は値が分かるまで判定できません。`defaults(v)` は型付きの値を返します: `defaults(0)` は `0`、`defaults('0')` は `"0"`、`defaults(null)` は `null`。
|
|
1622
|
+
|
|
1623
|
+
**引用符の規則。** 引用符(`'` / `"`)は「ここはリテラル」という宣言で、引数の文法は意図的に小さく保たれています:
|
|
1624
|
+
|
|
1625
|
+
- 引用符の中では `,` `;` `|` `:` はただの文字です。`join(', ')`・`join(': ')`・`join(';')` はそのまま通ります。
|
|
1626
|
+
- 空白のトリムは**引用符の外側だけ**です: `padStart(5, ' ')` は空白で埋め、`toFixed( 2 )` は `toFixed(2)` と同じです。
|
|
1627
|
+
- **エスケープ文字はありません。** 同じ種類の引用符をリテラルの中に入れることはできません — `'it\'s'` ではなく `"it's"` と書いてください(前者は未終端の引用符として `[wcs/binding-syntax]` で落ちます)。
|
|
1628
|
+
- 隣り合う並びは 1 つの引数に連結されます: `'a' 'b'` は 1 個の文字列 `a b`、`1'2'` は文字列 `12` です(引数のどこかに引用符があれば、その引数は文字列になります)。
|
|
1629
|
+
- 未終端の引用符は受理しません。末尾の空引数は落とされ(`filter()` は引数 0 個)、先頭・中間の空引数は位置を保ちます(`defaults(,)` は空文字 1 個を渡します)。
|
|
1600
1630
|
|
|
1601
1631
|
## Web Component バインディング
|
|
1602
1632
|
|
|
@@ -1712,6 +1742,7 @@ customElements.define("user-card", UserCard);
|
|
|
1712
1742
|
- 部分マウントを併用できます: `state: user; state.theme: theme` は `theme` を 2 つ目の入口としてマウントします(最長接頭辞が勝つので、中の `theme.mode` はツリーの `theme.mode` を読みます)
|
|
1713
1743
|
- ループでは**行そのもの**をマウントします: `<template data-wcs="for: users"><user-row data-wcs="state: ."></user-row></template>`。行コンポーネントの中の `name` は `users.*.name`、中の `for: tags` は `users.*.tags.*` を回します
|
|
1714
1744
|
- **自前のキーは私有**です([docs/state-mount-design.md](../../docs/state-mount-design.md) §4-3 の R1): コンポーネントが自分で宣言したデータキー(`state = { mode: "view" }`)はその要素のもので、ツリーには書かれません。マウント先に同名のキーがあってそれを隠す形(`user.name` の上に `state = { name: "" }`)では、ランタイムが 1 回だけ warn します(`wcs/mount-own-key-shadow`)— ツリーを読みたければ既定値を消し、私有のままにしたければ名前を変えてください
|
|
1745
|
+
- `element.state` から取り出したメソッド(`card.state.toggle()`)は、イベントのバインディング(`onclick: toggle`)から呼んだときと同じに動きます。自前のキーへの書き込みもツリーのキーへの書き込みも描き直され、行マウントではその行に着地し、同期メソッドは値を、async メソッドは Promise を返します
|
|
1715
1746
|
- 配列そのものをルートにマウントする形(`state: rows` + 中で `for`)は非対応です。行をマウントする(`state: .`)か、配列を持つオブジェクトをマウントして中で `for` を回してください(`state: group` + `for: children`)。どちらも契約テストで固定されており、マウントがツリー拡張の唯一の手段です
|
|
1716
1747
|
|
|
1717
1748
|
> プロパティ単位の形(`state.message: user.name`)はそのまま動きます — 同じ機構の上の部分マウントです。
|
|
@@ -1719,6 +1750,11 @@ customElements.define("user-card", UserCard);
|
|
|
1719
1750
|
> 宣言しているコンポーネント(`state = { message: "" }` + `state.message: ...`)もホストの値を読み、
|
|
1720
1751
|
> 既定値は使われません(2.x では R1 によって自前のキーが私有になり、ホストの値を隠していました —
|
|
1721
1752
|
> `wcs/mount-own-key-shadow` の警告付き)。マップしていない自前のキーは今も R1 で私有です。
|
|
1753
|
+
> これは**1 段のエントリ**の話です: より深いエントリ(`state.a.b: outer.b`)は `a` の私有性に
|
|
1754
|
+
> 触らないので、コンポーネントが `a` を自分で宣言していれば `a` とその配下は私有のままで、
|
|
1755
|
+
> エントリは届きません。名前がコンポーネントの getter / setter / メソッドと一致するエントリも同じで、
|
|
1756
|
+
> コンポーネント自身の面が勝ちます。いずれの形も `wcs/mount-own-key-shadow` で 1 回報告されます —
|
|
1757
|
+
> どちらかの名前を変えてください。
|
|
1722
1758
|
>
|
|
1723
1759
|
> **マウントの `#ro` を尊重します(3.0):** `state#ro: user` や `state.title#ro: doc.title` では、
|
|
1724
1760
|
> コンポーネントはそのエントリを読めても書けません — `element.state.title = …`・メソッド内の
|
|
@@ -1785,7 +1821,7 @@ customElements.define("my-component", MyComponent);
|
|
|
1785
1821
|
- `bind-component` 付きの `<wcs-state>` はコンポーネント要素の**直下**(トップレベル)に配置すること
|
|
1786
1822
|
- 親要素は**カスタム要素**(ハイフンを含むタグ名)であること
|
|
1787
1823
|
- Light DOM コンポーネントはホストからの配線が必須(plain 形は v2 で廃止)
|
|
1788
|
-
- **マウントされた**スコープは宣言面を実行しない。`$watch`・`$
|
|
1824
|
+
- **マウントされた**スコープは宣言面を実行しない。`$watch`・`$stream`・`$scan` は一度だけ警告を出して無視され、`$recursion` / `**` getter は拒否される。ルート state に宣言すること(ボリューム `<wcs-state mount>` は `$watch` を持てるが、`$scan` と `$recursion` はルート専用)。配線されていない Shadow DOM の子は独立したツリーを持つため、いずれも宣言できる
|
|
1789
1825
|
|
|
1790
1826
|
### ループ内でのコンポーネント使用
|
|
1791
1827
|
|
|
@@ -1800,6 +1836,8 @@ customElements.define("my-component", MyComponent);
|
|
|
1800
1836
|
</template>
|
|
1801
1837
|
```
|
|
1802
1838
|
|
|
1839
|
+
行の中のコンポーネントは自分の行を読み書きします。`$connectedCallback` / `$disconnectedCallback` の `this`(と、そこからタイマーや `await` の先へ取っておいた `this`)は要素の行に付いていきます — `if:` が要素を隠している間もその行に書き、要素の書き込み(`users[1] = …`)で行がその場で差し替わったら新しい行に書きます。要素がいったん外されて別の行に使い回された後は、取っておいた `this` はどちらの行にも書かずに `The host row of <user-row> was removed.` で投げます。行そのものが消えたときの `$disconnectedCallback` は、コンポーネント自身のキーを読み書きでき(`this.tid` に取っておいたタイマーの id を解除する)、行のキー(`this.name`)を読むと同じエラーで投げます。部分マウント(`state.name: .name`)の自身のキーは行ごとではなく要素ごとに 1 組なので、`$disconnectedCallback` がそこへ書いた値は、要素が別の行に使い回された後も残ります。
|
|
1840
|
+
|
|
1803
1841
|
### コンポーネント側でリストを描画する
|
|
1804
1842
|
|
|
1805
1843
|
配列をコンポーネントに束ね、`for:` をコンポーネントの内側で回すこともできます。
|
|
@@ -1838,7 +1876,7 @@ this.shadowRoot.innerHTML = `
|
|
|
1838
1876
|
|
|
1839
1877
|
コンポーネントの中にさらにコンポーネントを置いて、**スコープを重ねる**こともできます。途中のコンポーネントが配列を素通しするだけで自分では `for` を回さなくても、正本スコープ起点の行フィールド書き込みは最下層の行まで届きます。
|
|
1840
1878
|
|
|
1841
|
-
コンポーネントの作者が自分の置かれる深さを意識する必要はありません。`$1` / イベントハンドラのインデックス / `$
|
|
1879
|
+
コンポーネントの作者が自分の置かれる深さを意識する必要はありません。`$1` / イベントハンドラのインデックス / `$renderedCallback` / `$getAll` はいずれも**自分のスコープ内の位置**を報告します。
|
|
1842
1880
|
|
|
1843
1881
|
## Command Token(メソッドバインディング)
|
|
1844
1882
|
|
|
@@ -1952,9 +1990,12 @@ interface CommandToken {
|
|
|
1952
1990
|
|
|
1953
1991
|
`emit` は各購読者の戻り値の配列を(subscribe 順で)返します。`Promise` を返すメソッドは `Promise.all(token.emit(...))` でラップしてすべてを待ち受けてください。
|
|
1954
1992
|
|
|
1993
|
+
**1 つの購読者が throw しても、残りへの配信は止まりません。** token はファンアウトの口で購読者どうしは互いを知らないため、`emit` は全員を呼びます。投げた購読者は `console.error`(token 名つき)で報告され、結果配列のその位置は `undefined` になり、残りの購読者には届きます。例外は emitter へは**届きません** — state のイベントハンドラと同じ規則で、発火経路はハンドラの完了を待たず、DOM イベント起点では投げ返す呼び出し元が存在しないためです。呼び出し側に知らせたいときは throw ではなく戻り値(または reject した `Promise`)で返してください。`Promise` を返す購読者の reject はここでは捕捉しません。そのための `Promise.all(token.emit(...))` です。
|
|
1994
|
+
|
|
1955
1995
|
### 購読のライフサイクル
|
|
1956
1996
|
|
|
1957
|
-
- 購読者は要素を `WeakRef`
|
|
1997
|
+
- 購読者は要素を `WeakRef` で保持するので、token の購読者セット**それ自体**は要素を生かし続けない
|
|
1998
|
+
- **注意(現在の実装):** 購読者のクロージャは `binding` も捕捉しており、`binding.node` はその同じ要素 — 上の `WeakRef` を打ち消す強参照です。実際には、取り外された要素は binding が解放されるまで残ります。つまり `WeakRef` が効くのは「binding は消えたが token に購読者が残っている」場合だけです。行や要素が消えた時点で購読者を解放するのが本筋の修正ですが、binding 機構に要素ライフサイクルのフックが無く、その追加とセットで入れるべき変更なので 3.x には入れていません。**今日頼れる仕組みは下の lazy purge**だと考えてください
|
|
1958
1999
|
- `emit` 時、WeakRef が回収済みか要素が接続されていない(`isConnected === false`)場合、購読は自動的に破棄される(lazy purge)
|
|
1959
2000
|
- 所有する `<wcs-state>` が disconnect されても token レジストリは保持されるので、ルート `<wcs-state>` を付け直した後(ホストを DOM 上で移動したときなど)も購読に命令が届く。切断中は state を作れないので、`$command` 経由の emit は起きない
|
|
1960
2001
|
|
|
@@ -1980,7 +2021,7 @@ command token は state コードから emit する必要はありません。DO
|
|
|
1980
2021
|
|
|
1981
2022
|
これは純粋な配線です。イベント端点を command token 端点に接続するだけで、間にロジックは入りません。`emit` の引数はハンドラ呼び出しとまったく同じく透過されます —— まず DOM の `Event`、続いて内包するリストインデックス —— なので購読者は `(event, ...listIndexes)` を受け取ります。購読者の中で必要なものをイベントから取り出してください(`event.target.value`、`event.detail` など)。
|
|
1982
2023
|
|
|
1983
|
-
- 右辺は `$command.<name>` であり、`<name>` は `$commandTokens` で宣言されていること。`CommandToken` に解決されないパス(例: typo
|
|
2024
|
+
- 右辺は `$command.<name>` であり、`<name>` は `$commandTokens` で宣言されていること。`CommandToken` に解決されないパス(例: typo)はイベント時に失敗する。DOM のイベントには投げ返せないので、伝播ではなくイベントごとに 1 回 `console.error` で報告される(reject した state ハンドラと同じ着地)。
|
|
1984
2025
|
- 修飾子はそのまま機能する: `onclick#prevent: $command.someToken` は emit の前に `preventDefault()` を呼ぶ(`#stop` も同様)。
|
|
1985
2026
|
- これは state が emit するのと同じ token を emit するので、`command.<method>: $command.someToken` で配線された要素の購読者は、誰がトリガを引いたかに関わらず受け取る。
|
|
1986
2027
|
|
|
@@ -2142,22 +2183,22 @@ event token は command token と同じ `Token` pub/sub プリミティブを共
|
|
|
2142
2183
|
| 宣言 | 宣言するもの | 値を持つか | 発火 | 主な用途 |
|
|
2143
2184
|
|---|---|---|---|---|
|
|
2144
2185
|
| [パス getter](#パス-getter算出プロパティ) | その値が現在の state から見て**何であるか** | 持たない(アドレス単位で再計算・キャッシュ) | 評価需要が読んだときに遅延評価 | 小計、分類、集計 |
|
|
2145
|
-
| [`$
|
|
2186
|
+
| [`$stream`](#streamstream) | 非同期の供給元と、**1 回の実行の中で** fold される値 | 持つ(出力は runtime の所有) | chunk ごと。`args` が変われば `initial` に戻して restart | フィード、ソケット、継続的な観測 |
|
|
2146
2187
|
| [`$watch`](#watchwatch) | 変更への反応 | 持たない | 変化したアドレスごとにバッチ 1 回、scan の書き込みの後 | 副作用、「条件が成立したとき」 |
|
|
2147
2188
|
| [`$scan`](#scanscan) | 時間をまたぐ累積値と、それを戻す条件 | 持つ(出力は runtime の所有) | 着地ごと(`from`)またはイベントごと(`on`) | ページ蓄積、履歴、件数 |
|
|
2148
2189
|
|
|
2149
2190
|
混乱のほとんどは、次の 2 点で解けます。
|
|
2150
2191
|
|
|
2151
|
-
- **`$
|
|
2152
|
-
- **`$
|
|
2192
|
+
- **`$renderedCallback` はこの表に入りません。** 適用されたバインディングを報告するものなので、そこに処理をぶら下げると描画内容に暗黙に依存します。[評価のきっかけ](#評価のきっかけdemand-root)を参照してください。
|
|
2193
|
+
- **`$stream` の fold は restart のたびに戻り、`$scan` は戻りません。** restart を越えて値を保ちたいとき、あるいは状態ではなく出来事を数えたいときは `$scan` の領分です。
|
|
2153
2194
|
|
|
2154
|
-
## Stream(`$
|
|
2195
|
+
## Stream(`$stream`)
|
|
2155
2196
|
|
|
2156
2197
|
> **3.2 から宣言キーの正式名は `$stream` です**(要件 B12 — `$watch` / `$scan` と単数で揃えた)。旧名 `$streams` は 3.x の間は同じに動き、4.0 で外れます。両方を宣言すると `[wcs/declaration-alias]` で落ちます。`$streamStatus` / `$streamError` の名前空間は変わりません。3.1 以前では `$streams` と書いてください。
|
|
2157
2198
|
|
|
2158
|
-
command token / event token が運ぶのは離散的なやり取りです。**`$
|
|
2199
|
+
command token / event token が運ぶのは離散的なやり取りです。**`$stream`** は残る形 —— 連続的なフローをカバーします。非同期 producer(async iterable / async generator / `ReadableStream`)を宣言すると、フレームワークがそれを **fold して単一の reactive プロパティに畳み込みます** —— 各チャンクは通常のパス代入を通るため、バインディング・パス getter・`$renderedCallback` は自分で値を代入した場合とまったく同じように反応します。`args` 関数が読んだ state パスが変化すると、実行中の producer は abort され、新しい引数で source が張り直されます(switchMap 型の依存駆動 restart)。stream は `$connectedCallback` 完了後に eager に起動し、要素の disconnect で abort されます。
|
|
2159
2200
|
|
|
2160
|
-
`$
|
|
2201
|
+
`$renderedCallback` は引き続き binding 駆動です。stream 宣言だけでは headless な購読にならず、その value/status/error の live DOM binding が実際に適用されたときだけ callback の path に現れます。描画せずに stream の値へ反応したい場合は、そのパスに [`$watch`](#watchwatch) を宣言してください。観測契約は [stream リファレンス](docs/streams.md) を参照してください。
|
|
2161
2202
|
|
|
2162
2203
|
```html
|
|
2163
2204
|
<wcs-state>
|
|
@@ -2165,7 +2206,7 @@ command token / event token が運ぶのは離散的なやり取りです。**`$
|
|
|
2165
2206
|
export default {
|
|
2166
2207
|
prompt: "",
|
|
2167
2208
|
|
|
2168
|
-
$
|
|
2209
|
+
$stream: {
|
|
2169
2210
|
// フル形: LLM トークンストリームを累積
|
|
2170
2211
|
tokens: {
|
|
2171
2212
|
args: (state) => state.prompt, // 依存はここでのみ捕捉される
|
|
@@ -2204,7 +2245,7 @@ error 時、プロパティは直前の fold 結果を保持し、エラーは `
|
|
|
2204
2245
|
**イベント API の橋渡し** —— 実際の source の多くはコールバック型(`EventSource`・`WebSocket`・DOM イベント)で、async iterable ではありません。標準の `ReadableStream` で包んでください: `start` で enqueue し、`cancel` でリソースを解放します。`AbortSignal` には一切触れません —— restart / 破棄時は runtime が reader を cancel し、parked read を強制解放して `cancel()` まで届けます:
|
|
2205
2246
|
|
|
2206
2247
|
```js
|
|
2207
|
-
$
|
|
2248
|
+
$stream: {
|
|
2208
2249
|
metrics: {
|
|
2209
2250
|
args: (state) => ({ host: state.host }),
|
|
2210
2251
|
source: ({ host }) => {
|
|
@@ -2230,6 +2271,7 @@ $streams: {
|
|
|
2230
2271
|
- **有界 fold** —— 需要は producer に逆流しません(backpressure は明示的に放棄)。無限 / 長寿命ストリームでは latest・count・last-N(`(acc, chunk) => [...acc.slice(-99), chunk]`)・ウィンドウ集計など有界な fold を使うこと。生の全チャンク累積は有限ストリーム限定。
|
|
2231
2272
|
- **`args` は同期** —— Promise を返すとエラー。`args` 内での wildcard 読みも拒否されます。
|
|
2232
2273
|
- **自己依存・相互サイクルの禁止** —— `args` が自 stream の値や status を読むとエラーになります。2 つの stream の相互サイクル(A の `args` が B の値を読み、B の `args` が A の値を読む)は検出されず無限 restart になるため組まないこと。一方向のチェイン(A の値を B の `args` が読む)は正当です。
|
|
2274
|
+
- **プロパティは runtime の所有物** —— エントリ名が state の getter / setter / メソッドと衝突すると、`$scan` の出力と同じく宣言時にエラーになります(検査が無いと、そのメソッドは起動時の `initial` リセットで無言に上書きされます)。
|
|
2233
2275
|
- **SSR では起動しない** —— サーバーでは宣言のパースとプロパティの実体化(`initial`)のみ行い、source は実行されません。クライアント側は通常どおり起動します。
|
|
2234
2276
|
|
|
2235
2277
|
完全な契約 —— ライフサイクルと所有権・restart セマンティクス・flush 粒度・スコープ外リスト —— は [docs/streams.ja.md](docs/streams.ja.md) を参照してください。
|
|
@@ -2244,9 +2286,9 @@ $streams: {
|
|
|
2244
2286
|
|---|---|---|
|
|
2245
2287
|
| **live DOM バインディング** | `data-wcs` / mustache / コメントバインディング | **する**(その要素が消えると需要も消える) |
|
|
2246
2288
|
| **`$watch` の宣言** | state 側 | しない(headless) |
|
|
2247
|
-
| **`$
|
|
2289
|
+
| **`$stream` の `args`** | state 側 | しない(起動・restart のたびに評価される) |
|
|
2248
2290
|
|
|
2249
|
-
**`$
|
|
2291
|
+
**`$renderedCallback` は根ではありません。** それは「バインディングが適用された結果」の報告であり、需要を作りません。
|
|
2250
2292
|
|
|
2251
2293
|
### 描画がプログラムの意味論を変えうる
|
|
2252
2294
|
|
|
@@ -2258,27 +2300,27 @@ $streams: {
|
|
|
2258
2300
|
```
|
|
2259
2301
|
|
|
2260
2302
|
```javascript
|
|
2261
|
-
// $
|
|
2303
|
+
// $renderedCallback は binding 駆動 —— 上の <b> を消すと paths に現れなくなり、
|
|
2262
2304
|
// フィードの commit が黙って止まる
|
|
2263
|
-
$
|
|
2305
|
+
$renderedCallback(paths) {
|
|
2264
2306
|
if (!paths.includes("$streamStatus.pageResult")) return;
|
|
2265
2307
|
this.items = this.items.concat(this.pageResult.items);
|
|
2266
2308
|
}
|
|
2267
2309
|
```
|
|
2268
2310
|
|
|
2269
|
-
**規則:** 描画に依存させたくないロジックは、`$watch`・`$scan`・`$
|
|
2311
|
+
**規則:** 描画に依存させたくないロジックは、`$watch`・`$scan`・`$stream` の `args` のどれかに根を置いてください。`$renderedCallback` は「描かれたものに追随する」用途に限ります。
|
|
2270
2312
|
|
|
2271
|
-
上の例はいまは `$scan` で feed を積み(sentinel の再武装は `$watch`)、`<b>` は表示専用に戻っています。この形(`$
|
|
2313
|
+
上の例はいまは `$scan` で feed を積み(sentinel の再武装は `$watch`)、`<b>` は表示専用に戻っています。この形(`$renderedCallback` が、どのバインディングにも現れないパスを判定に使っている)は **`wcs/updated-callback-unbound`** として静的に検出されます。
|
|
2272
2314
|
|
|
2273
2315
|
### 残る制約
|
|
2274
2316
|
|
|
2275
|
-
需要の根が 3 か所に分かれること自体は変わりません。**ある getter が評価されるかを知るには、その 3 か所(ページの全バインディング・全 `$watch`・全 `$
|
|
2317
|
+
需要の根が 3 か所に分かれること自体は変わりません。**ある getter が評価されるかを知るには、その 3 か所(ページの全バインディング・全 `$watch`・全 `$stream.args`)を見る必要があり、getter の定義だけを読んでも分かりません。** lint と DevTools の配線カバレッジはこの照合を機械にやらせるためのものです。
|
|
2276
2318
|
|
|
2277
2319
|
なお `$watch` に宣言したスカラー getter は **eager** になります(接続時に 1 回、以後は依存に触れたバッチごとに評価)。ワイルドカード行の getter は eager 化しません(初回評価がリスト全体を舐めるため)。
|
|
2278
2320
|
|
|
2279
2321
|
## Watch(`$watch`)
|
|
2280
2322
|
|
|
2281
|
-
`$
|
|
2323
|
+
`$renderedCallback` は **binding 駆動** です。その更新で live DOM binding が実際に適用された path だけを報告するため、**描画していない値の変化は見えません**。**`$watch`** はその headless 版で、ページ上でそのパスがバインドされているかどうかに関わらず、state の変化で発火します(**ワイルドカードの行パスだけは例外**で、headless に成立させるには `$listKeys` が要ります。後述)。
|
|
2282
2324
|
|
|
2283
2325
|
```html
|
|
2284
2326
|
<wcs-state>
|
|
@@ -2323,11 +2365,11 @@ $updatedCallback(paths) {
|
|
|
2323
2365
|
|
|
2324
2366
|
| 層 | 順序 | 制御 |
|
|
2325
2367
|
|---|---|---|
|
|
2326
|
-
| 機構間 | `$
|
|
2368
|
+
| 機構間 | `$renderedCallback` → `$scan` → `$watch` → `$stream` restart | 固定 |
|
|
2327
2369
|
| ハンドラ間 | `$watch` の宣言順 | **宣言を並べ替える** |
|
|
2328
2370
|
| 同一パスの行間 | `indexes` 昇順 | 固定 |
|
|
2329
2371
|
|
|
2330
|
-
**機構間の層を動かす唯一のもの**が、`state` 参加者を受け付ける `<wcs-view-transition>` です。バインディング適用 —— したがって `$
|
|
2372
|
+
**機構間の層を動かす唯一のもの**が、`state` 参加者を受け付ける `<wcs-view-transition>` です。バインディング適用 —— したがって `$renderedCallback` —— がフレームで着地する一方、`$scan`・`$watch`・`$stream` restart は state アドレスを消費し DOM を見ないので、drain がキューされた microtask に留まります。タグがある間の順序は `$scan` → `$watch` → `$stream` restart → `$renderedCallback` です。この層を並べ替えるものはページ上でこれ 1 つだけです。[docs/timing-and-firing-contract.ja.md](https://github.com/wcstack/wcstack/blob/main/docs/timing-and-firing-contract.ja.md) §4.3 を参照してください。
|
|
2331
2373
|
|
|
2332
2374
|
主なルール:
|
|
2333
2375
|
|
|
@@ -2335,15 +2377,15 @@ $updatedCallback(paths) {
|
|
|
2335
2377
|
- **中間値は観測できません** —— 1 バッチ内の `a → b → c` は `cur = c` / `prev = a` で 1 回だけ発火します(binding 更新と同じ契約)。
|
|
2336
2378
|
- **行は drain の時点のリストに従います** —— 同じ job で行を書いてから取り除いた・置き換えた・リストを短くした行は発火せず、位置だけが移った行も発火しません。1 つの位置が発火するのは多くても 1 回です。入れ子のリストを置き換えると、新しい配列の行がすべて発火します。
|
|
2337
2379
|
- **行単位の差分を見たいなら `$listKeys`** —— 未宣言のまま配列全体を代入すると、行 watch は**全行**について `prev === undefined` で発火します(どの行もパス書き込みを通っていないため)。`$listKeys` を宣言すればキー突合が per-field 書き込みに分解するので、変化した行だけが発火し `prev` もスカラで取れます。
|
|
2338
|
-
- **headless な行 watch には `$listKeys` が必要** —— `$watch` が単独では headless にならない唯一の箇所です。`items` から `items.*.price` への展開はリストの `for` バインディングが駆動しており、watch を宣言してもそのパスをリストとしては登録しません(意図的)。したがって `for` バインドも `$listKeys` も無い状態で配列を代入すると、行 watch は**一度も**発火しません。`$listKeys` を宣言する(キー突合がフィールドごとにパス書き込みするので展開を経由しない)か、リストを描画してください。スカラーパスは `user.name` のようなネストしたものも含め、この条件なしに headless
|
|
2339
|
-
- **ハンドラの例外は隔離されます** —— throw はコンソールに報告され、残りの watch(と stream の restart)は続行します。loud fail する `$connectedCallback` / `$
|
|
2340
|
-
- **書き込みの連鎖には上限があります** —— ハンドラの書き込みは新しいバッチを作るため、相互に書き合う watch は無限ループになり得ます。32 段で打ち切り、コンソールに報告します(値と DOM
|
|
2341
|
-
- **マウントされた `bind-component` スコープでは実行されません** —— マウントされたコンポーネントは宣言面を実行せず、`$watch` の宣言があると 1 回だけ console.warn でルート state(またはボリューム —— `<wcs-state mount>` は `$watch` / `$listKeys` / `$
|
|
2380
|
+
- **headless な行 watch には `$listKeys` が必要** —— `$watch` が単独では headless にならない唯一の箇所です。`items` から `items.*.price` への展開はリストの `for` バインディングが駆動しており、watch を宣言してもそのパスをリストとしては登録しません(意図的)。したがって `for` バインドも `$listKeys` も無い状態で配列を代入すると、行 watch は**一度も**発火しません。`$listKeys` を宣言する(キー突合がフィールドごとにパス書き込みするので展開を経由しない)か、リストを描画してください。スカラーパスは `user.name` のようなネストしたものも含め、この条件なしに headless で発火します。`for` が無ければ、同じ配列やその写しの代入と `$postUpdate("items")` でも発火しません。行の値を読み直させるだけなので、それを読む getter(`$getAll("items.*.price", [])`)はその場で変えた値を読みます。
|
|
2381
|
+
- **ハンドラの例外は隔離されます** —— throw はコンソールに報告され、残りの watch(と stream の restart)は続行します。loud fail する `$connectedCallback` / `$renderedCallback` とは異なる扱いです。
|
|
2382
|
+
- **書き込みの連鎖には上限があります** —— ハンドラの書き込みは新しいバッチを作るため、相互に書き合う watch は無限ループになり得ます。32 段で打ち切り、コンソールに報告します(値と DOM は巻き戻しません)。段は書き込みごとに数えます。ハンドラ(と `$scan` の畳み)は自分を起こした書き込みの深さから始まるので、連鎖を伸ばすのは、ハンドラの書き込みが別のハンドラを起こしたときだけです。長い有限の描画の連鎖(`$renderedCallback` が描画ごとに見出しを 1 段ずつ詰める)を見るハンドラは、自分の書き込みが同じバッチに載っても打ち切られません。`$stream` の再開もハンドラと同じく数えます。再開の書き込み(`initial` への戻し・status)は再開を起こした書き込みの連鎖の続きで、上限を越えた書き込みが起こす再開は行いません。そのため、stream の `args` へ書き、再開でまた起こされる `$watch` も、`args` で互いの値を読む stream どうしも打ち切られます —— 上限を越えたバッチは、`$watch` を宣言していなくても報告されます。
|
|
2383
|
+
- **マウントされた `bind-component` スコープでは実行されません** —— マウントされたコンポーネントは宣言面を実行せず、`$watch` の宣言があると 1 回だけ console.warn でルート state(またはボリューム —— `<wcs-state mount>` は `$watch` / `$listKeys` / `$renderedCallback` を持てます)へ誘導します(`$stream` も同様)。plain な(配線なし Shadow の)子は独立ツリーを持つので宣言できます。
|
|
2342
2384
|
- **SSR では実行されません** —— ハンドラの副作用がサーバーとクライアントで二重に走るためです。
|
|
2343
2385
|
|
|
2344
2386
|
## Scan(`$scan`)
|
|
2345
2387
|
|
|
2346
|
-
`$
|
|
2388
|
+
`$stream` が畳むのは 1 回の run の**内側**で、restart のたびに値は `initial` へ戻ります。`$watch` は値を所有しません。**`$scan`** はその両方を跨いで残る値 —— 時間軸方向の累積 —— を、持ち主・発火単位・reset 条件つきで宣言します。
|
|
2347
2389
|
|
|
2348
2390
|
```html
|
|
2349
2391
|
<wcs-state>
|
|
@@ -2352,7 +2394,7 @@ $updatedCallback(paths) {
|
|
|
2352
2394
|
page: 1,
|
|
2353
2395
|
host: "a",
|
|
2354
2396
|
$eventTokens: ["message"],
|
|
2355
|
-
$
|
|
2397
|
+
$stream: {
|
|
2356
2398
|
pageResult: { args: (s) => s.page, source: loadPage },
|
|
2357
2399
|
},
|
|
2358
2400
|
$scan: {
|
|
@@ -2388,7 +2430,7 @@ $updatedCallback(paths) {
|
|
|
2388
2430
|
| `fold` | 必須。`from` は `(acc, cur, prev, ...indexes) => next`、`on` は `(acc, event, ...indexes) => next`。同期で、`this` 無しで呼ばれ、新しい値を返す。`acc` そのものを返すと書き込まない。 |
|
|
2389
2431
|
| `resetOn` | 任意。素の state パスの配列。どれかが書かれたら出力を `initial` に戻す。`from` の scan はそのバッチの fold を行わず、`on` の scan は書き込みより後に来たイベントを `initial` から畳む。`from` の配下は raise、`from` の祖先は可(親の差し替えで作り直す)。オブジェクトのパスはそのオブジェクト自身が書かれたときだけ reset し、子への書き込みでは reset しない(葉のパスを並べるか nonce を使う)。 |
|
|
2390
2432
|
|
|
2391
|
-
**出力はランタイムが所有します**(`$
|
|
2433
|
+
**出力はランタイムが所有します**(`$stream` の値と同じ)。state にそのプロパティが無ければ `initial` で実体化され(plain なデータは複製するので、出力の plain な部分の子パスへ書いても宣言の `initial` は変わりません。クラスのインスタンスや凍結された値など plain でない値は宣言と共有したままです)、他のパスと同じようにバインドできます。stream の restart・切断と再接続・同じオブジェクトの再セットを跨いで残り、新しい宣言での再セットでは作り直されます。出力名が getter・setter・メソッド・`$stream` のエントリと衝突すると raise します。
|
|
2392
2434
|
|
|
2393
2435
|
2 つの source の発火:
|
|
2394
2436
|
|
|
@@ -2403,8 +2445,8 @@ $updatedCallback(paths) {
|
|
|
2403
2445
|
- **getter を畳まない。** getter は入力が変わるたびに再評価されるので、畳むと出来事ではなく再評価の回数を数えます。`from` や `resetOn` に getter を書く(`from` に `$recursion` の `**` getter の展開形 `nodes.*.total` を書くのも同じ)と、宣言時に raise します(`wcs/scan-source-computed`)。
|
|
2404
2446
|
- **1 回の fold は着地ごとで、ページごとではない。** `done` 後の再試行や、ページの再接続は同じページをもう一度着地させます。問題になるなら fold に冪等キーを持たせてください(上の `pages`)。
|
|
2405
2447
|
- **stream の `args` を自分の scan 出力から導出しない。** `feed` から導出した getter(`feed` を畳む別の scan の出力から導出したものを含む)を `pageResult` の `args` が読むと、stream が自分の結果で restart し続けるので、ランタイムは `wcs/scan-feedback-loop` を raise します。カーソルはイベントから進めてください。stream の restart と同じバッチに着地した chunk は abort される run のものなので畳みません。
|
|
2406
|
-
- **要素の出来事は `on` で受ける。** `from` はそのパスへの書き込みをすべて見ます。バインドした要素の初期同期や、親オブジェクトの丸ごと書き(`prev` は `undefined`)も 1 回として畳みます。`prev` は `$watch` と同じ台帳なので、`$scan` / `$watch` のリスナーの中の書き込み(`$watch` ハンドラや、`from` にした別の scan の出力)でも `undefined` です。台帳はそのリスナーの終わりに消えるので、同じ drain でその後に走る `$
|
|
2407
|
-
- **fold は有界に。** 無限の source は有界な値(直近 N 件・件数)に畳んでください(`$
|
|
2448
|
+
- **要素の出来事は `on` で受ける。** `from` はそのパスへの書き込みをすべて見ます。バインドした要素の初期同期や、親オブジェクトの丸ごと書き(`prev` は `undefined`)も 1 回として畳みます。`prev` は `$watch` と同じ台帳なので、`$scan` / `$watch` のリスナーの中の書き込み(`$watch` ハンドラや、`from` にした別の scan の出力)でも `undefined` です。台帳はそのリスナーの終わりに消えるので、同じ drain でその後に走る `$stream` の restart の書き込みは `prev` を持ちます。
|
|
2449
|
+
- **fold は有界に。** 無限の source は有界な値(直近 N 件・件数)に畳んでください(`$stream` と同じ)。
|
|
2408
2450
|
- **例外は隔離される。** throw・Promise の戻り値・読めない値はコンソールと DevTools に報告され、書き込みません(ワイルドカードの `from` で読めない行はその行だけを飛ばし、行の着地はリストの位置 1 つにつき 1 回に絞ります)。他の scan・watch・stream の restart は続行します。
|
|
2409
2451
|
- **`$watch` は scan の書き込みの後に走る。** 同じ drain の `$watch` ハンドラは畳んだ後の出力を読み、ハンドラが出力へ書いた値はそのまま残ります。出力の着地が drain される前に `from` の source がもう一度書かれる(その drain の `$watch` ハンドラが書くなど)と、両方が同じバッチに載ります。このとき出力を見る `$watch` は `prev` に着地した値を受け、`cur` に 1 段先の値を見て、次のバッチで同じ値でもう一度発火することがあるので、同じ値の重複に耐える形にしてください。ユーザー操作で累積を消すなら、`resetOn` に nonce を読ませてください。
|
|
2410
2452
|
- **ルートのみ。** ボリューム(`mount=`)は `$scan` を拒否し、マウントされた `bind-component` スコープは 1 回の warn で無視します。SSR では `from` は畳みません(出力の実体化は行います)。
|
|
@@ -2546,7 +2588,7 @@ export default {
|
|
|
2546
2588
|
- 非空文字列でないエントリ
|
|
2547
2589
|
- `$` 始まりのエントリ(内部プロパティはコンポーネントの prototype に公開されません)
|
|
2548
2590
|
- 重複したエントリ — これは従来サイレントに壊れていました。重複名があると `wcBindable` 宣言全体が読み取り不能になり、その要素が黙って双方向バインド不可になります
|
|
2549
|
-
- 状態に存在しないエントリ(自身とプロトタイプチェーンの両方を探索します。`$
|
|
2591
|
+
- 状態に存在しないエントリ(自身とプロトタイプチェーンの両方を探索します。`$stream` の名前は値プロパティがインスタンスごとに実体化されるため「存在する」と見なされます)
|
|
2550
2592
|
- `$bindables` にメソッドを書いた場合、または `$commands` に値プロパティを書いた場合
|
|
2551
2593
|
|
|
2552
2594
|
### DCC のメソッドを起動する
|
|
@@ -2606,7 +2648,7 @@ export default {
|
|
|
2606
2648
|
| `$commands` | 起動可能メソッドの宣言 |
|
|
2607
2649
|
| `$connectedCallback` | ライフサイクルフック(各インスタンスで実行) |
|
|
2608
2650
|
| `$disconnectedCallback` | クリーンアップフック |
|
|
2609
|
-
| `$
|
|
2651
|
+
| `$renderedCallback` | 状態変更後に呼ばれる |
|
|
2610
2652
|
|
|
2611
2653
|
## SVG サポート
|
|
2612
2654
|
|
|
@@ -2622,7 +2664,7 @@ export default {
|
|
|
2622
2664
|
|
|
2623
2665
|
## ライフサイクルフック
|
|
2624
2666
|
|
|
2625
|
-
状態オブジェクトに `$connectedCallback` / `$disconnectedCallback` / `$
|
|
2667
|
+
状態オブジェクトに `$connectedCallback` / `$disconnectedCallback` / `$renderedCallback` / `$errorCallback` を定義すると、初期化・クリーンアップ・更新時・バインディング失敗時のフックとして利用できます。
|
|
2626
2668
|
|
|
2627
2669
|
> **3.2 から `$updatedCallback` の正式名は `$renderedCallback` です**(要件 B12)。このフックが受けるのは**適用されたバインディング**の更新で、state の更新全体ではない(それには `$watch`)ことを名前で言うためです。旧名は 3.x の間は同じに動き、4.0 で外れます。両方を宣言すると `[wcs/declaration-alias]` で落ちます。3.1 以前では `$updatedCallback` と書いてください。
|
|
2628
2670
|
|
|
@@ -2653,15 +2695,15 @@ export default {
|
|
|
2653
2695
|
|---|---|---|
|
|
2654
2696
|
| `$connectedCallback` | 初回接続時は状態初期化後、再接続時は毎回呼び出し | 可(await される) |
|
|
2655
2697
|
| `$disconnectedCallback` | 要素が DOM から削除された時 | 不可(同期のみ) |
|
|
2656
|
-
| `$
|
|
2657
|
-
| `$errorCallback(error, info)` | バインディングの適用に失敗した drain の後 — 失敗した本数ぶん、`$
|
|
2698
|
+
| `$renderedCallback(paths, indexesListByPath)` | live binding に更新が適用された後に呼び出し | 可(await されない) |
|
|
2699
|
+
| `$errorCallback(error, info)` | バインディングの適用に失敗した drain の後 — 失敗した本数ぶん、`$renderedCallback` の後に呼び出し | 可(await されない) |
|
|
2658
2700
|
|
|
2659
2701
|
`$disconnectedCallback` を除くすべてのフックで `async` を使用できます。リアクティブ Proxy はすべてのプロパティへの代入を変更として検知します。そのため、標準の `async/await` による処理とプロパティへの直接代入だけで非同期ロジックが完結します。ローディングフラグの切り替え、取得したデータの格納、エラーメッセージの更新といった処理もすべて単なるプロパティ代入で行えるため、非同期状態を管理するための複雑な抽象化機能は必要ありません。
|
|
2660
2702
|
|
|
2661
2703
|
- フック内の `this` は読み書き可能な状態プロキシです。
|
|
2662
2704
|
- `$connectedCallback` は要素が接続される**たびに**呼ばれます(一度削除された後の再接続も含みます)。再確立が必要なセットアップ処理に適しています。
|
|
2663
2705
|
- `$disconnectedCallback` は同期的に呼び出されます。タイマーのクリア、イベントリスナーの削除、リソースの解放といったクリーンアップ処理に使用してください。
|
|
2664
|
-
- `$
|
|
2706
|
+
- `$renderedCallback(paths, indexesListByPath)` は、その drain で live binding が適用された path の一覧を受け取ります。binding のない state 書き込みでは呼ばれず、`paths` にも現れません。ワイルドカードをもつパスが更新された場合は、`indexesListByPath` から対象のインデックス情報も取得可能です。マウントされたコンポーネントのマーカーパス(`#m…`)は `paths` に現れません — コンポーネントの私有キーは私有のままです(DevTools の overlays 表示で見えます)。`async` を使用できますが、戻り値は await されません。
|
|
2665
2707
|
- `$errorCallback(error, info)` はバインディングの**ページ内エラー境界**です。バインディングの適用が throw したとき(パス getter やフィルタが throw した、構造ディレクティブが失敗した)、その失敗は隔離され(同じバッチの残りは適用され、値も DOM も巻き戻されません)、このフックが無ければ `console.error` で報告されます。フックを宣言すると報告はそこへ届きます: `error` は throw された値、`info` はバインディングを識別する `{ path, bindingType, node }`(`path` は `data-wcs` に書いた形のまま。ワイルドカードもそのまま)。`this` は書き込み可能な state proxy なので、メッセージを state に書いて普通に描画するのが基本形です:
|
|
2666
2708
|
|
|
2667
2709
|
```js
|
|
@@ -2674,7 +2716,7 @@ export default {
|
|
|
2674
2716
|
};
|
|
2675
2717
|
```
|
|
2676
2718
|
|
|
2677
|
-
フックはバッチの後(`$
|
|
2719
|
+
フックはバッチの後(`$renderedCallback` の後)に走り、await されず、フック内で throw しても console に報告されるだけで drain は壊れません。DevTools にはフックの有無に関わらず全失敗が `state:binding-apply-error` として届きます。ルート専用で、ボリューム(`<wcs-state mount>`)に宣言しても無視されます。`$watch` ハンドラの失敗(別途隔離・報告)や、`$connectedCallback` / `$renderedCallback` が投げた例外(loud に失敗する)は対象外です。
|
|
2678
2720
|
- Web Component を使用している場合は、コンポーネント側に `async $stateReadyCallback(stateProp)` を定義おくことで、`bind-component` でバインドした状態が利用可能になった瞬間にフックとして呼び出されます。
|
|
2679
2721
|
|
|
2680
2722
|
## 遷移アニメーション
|
|
@@ -2697,8 +2739,8 @@ li {
|
|
|
2697
2739
|
|
|
2698
2740
|
そのタグが `state` 参加者を受け付けている間、知っておくべき帰結が 2 つある。
|
|
2699
2741
|
|
|
2700
|
-
- drain は microtask ではなくフレームで着地する。state に書いてから `await Promise.resolve()` で DOM を読むコードは遷移を待つ必要がある。`$
|
|
2701
|
-
- `$scan`・`$watch`・`$
|
|
2742
|
+
- drain は microtask ではなくフレームで着地する。state に書いてから `await Promise.resolve()` で DOM を読むコードは遷移を待つ必要がある。`$renderedCallback` はバインディング適用の直後という*位置*こそ変わらないが、その適用ごと 1 フレーム後ろへずれる。
|
|
2743
|
+
- `$scan`・`$watch`・`$stream` restart は元の microtask に留まるため、`$renderedCallback` の**前**に走るようになる。
|
|
2702
2744
|
|
|
2703
2745
|
適用すべきバインディングが実際にあるバッチだけがタグへ渡されるので、headless なパスへの書き込みが遷移を起こすことはない。タグが無ければ drain は従来どおり。[docs/timing-and-firing-contract.ja.md](https://github.com/wcstack/wcstack/blob/main/docs/timing-and-firing-contract.ja.md) §4.3 参照。
|
|
2704
2746
|
|
|
@@ -2742,7 +2784,7 @@ dropped. Validate statically: npx @wcstack/lint <file>.
|
|
|
2742
2784
|
| `wcs/getter-cycle` | パス getter どうしが循環参照していないか。実行時は「アドレススタックが既に積んでいるアドレスへ戻る」ことで判定する | 循環を断つ |
|
|
2743
2785
|
| `wcs/getter-depth-exceeded` | getter の評価が 1 パスで評価できる深さ(128 段)を超え、かつ同じアドレスを 2 度通っていない = データが単に深い | 集計の段数を減らすか、木を平らにする |
|
|
2744
2786
|
| `wcs/index-param-range` | `$N` は実在するワイルドカード段を指すこと(`$1`〜`$128`・先頭ゼロ不可) | 実在する段を使う |
|
|
2745
|
-
| `wcs/recursion-unsupported` | `**` を解釈しない場所へ `**` が渡った —— markup・`$watch` / `$listKeys` のキー・`$resolve` / `$postUpdate` / `$
|
|
2787
|
+
| `wcs/recursion-unsupported` | `**` を解釈しない場所へ `**` が渡った —— markup・`$watch` / `$listKeys` のキー・`$resolve` / `$postUpdate` / `$dependOn`・代入、あるいは state が `$recursion` を宣言していない | 具体パスを使うか、アンカーを宣言する |
|
|
2746
2788
|
| `wcs/recursion-declaration-invalid` | `$recursion` 宣言か `**` getter のキーが、このバージョンが受け付けない形 —— 要素を指さない・途中に添字セグメントを持つ(`"nodes.0.items.*"`)アンカー / 反復サブパス、複数アンカー、getter でない・setter を持つ `**` キー、`get "nodes.**"`、構造を名指す getter、同じ具体パスへ展開する 2 本の getter、展開形と同名の具体 getter。lint が先に出し、実行時は宣言を読んだ時点で throw する | 文面のとおり宣言を直す |
|
|
2747
2789
|
| `wcs/recursion-anchor` | 宣言済みのアンカーと合致しない `**` パス(このバージョンは state ごとに単一の自己再帰アンカー)、または `**` の後ろが整形されていない —— 空セグメント(`nodes.**.` / `nodes.**..x`)や `**` 直後の素の `*`(`nodes.**.*`) | 宣言どおりに綴り、その後ろに実在するパスを書く |
|
|
2748
2790
|
| `wcs/recursion-context` | **束縛**形の `**` を、束縛先の深さが無い場所で読んだ —— トップレベル、またはアンカー外の getter | 再帰 getter か行 getter の中から読むか、`[]` で全深さを合併する |
|
|
@@ -2768,7 +2810,7 @@ this.$getAll("matrix.*.*", [row]);
|
|
|
2768
2810
|
|
|
2769
2811
|
### バインディング 1 本の失敗は 1 本に閉じ込められます
|
|
2770
2812
|
|
|
2771
|
-
バインディングの適用が throw しても、そのバッチの残り・`$
|
|
2813
|
+
バインディングの適用が throw しても、そのバッチの残り・`$renderedCallback`・`$watch`・`$stream` の restart はすべて続行します。失敗は握り潰されず、`console.error` と DevTools(`state:binding-apply-error`)に出ます。
|
|
2772
2814
|
|
|
2773
2815
|
```
|
|
2774
2816
|
[@wcstack/state] binding "text: items.*.label" failed to apply; the rest of this batch continues.
|
|
@@ -2776,6 +2818,20 @@ this.$getAll("matrix.*.*", [row]);
|
|
|
2776
2818
|
|
|
2777
2819
|
隔離しない場合、1 本の throw が「値は新しいのに DOM は途中まで」という半端な状態を作り、しかも `$watch` と stream の restart が丸ごと消えていました(README のこの下にある発火順の契約が黙って破れる)。
|
|
2778
2820
|
|
|
2821
|
+
### 描画中の書き込みは無限ループになりません
|
|
2822
|
+
|
|
2823
|
+
バインディングの適用の最中に起きる書き込みがあります。`for:` の新しい行の中の wc-bindable 要素が初期値を state へ渡す(出力専用メンバー・`#init=element`)ときや、`$renderedCallback` が書くときです。こうした書き込みはそれぞれ新しいバッチを作り、そのバッチがまた描画されます。描画が毎回新しい値を書く形 —— たとえば行の要素の出力を、その一覧の getter が読むキーそのものへ束ね、要素がインスタンスごとに違う値を返す —— では、microtask が回り続けてページが固まります。この連鎖は 100 段で打ち切ります。101 段目になるバッチは描画せず(値は state に残ります)、そのバッチに載っていたパスを 1 回だけ報告します:
|
|
2824
|
+
|
|
2825
|
+
```
|
|
2826
|
+
[@wcstack/state] render chain depth limit exceeded; bindings for this batch were not applied. { maxDepth: 100, paths: ["mode", "view", "view.*", "view.*.m"] }
|
|
2827
|
+
```
|
|
2828
|
+
|
|
2829
|
+
上限がほかの 2 つ(32)より大きいのは、ここの 1 段が 1 回の描画だからです。描いて測って直す連鎖は、32 段を超えても正当に収まります —— 見出しの文字サイズを 64px から 18px まで 1px ずつ詰めると描画は 47 回、400 行を 10 行ずつ描くと 40 段です。
|
|
2830
|
+
|
|
2831
|
+
連鎖を伸ばすのは、バインディングの適用中に**同期で**起きた書き込みだけです。バインディングが要素のプロパティを設定したとき、要素がその場で同期に出したイベントもこれに含まれます。drain の外から届く書き込み —— 利用者の操作・非同期に届く I/O ノードのイベント・`$stream` の値・`await` の続き —— は 0 から数え直させます。`$scan`・`$watch`・`$stream` の restart の書き込みは、伸ばしも数え直させもせず、連鎖をそのまま次のバッチへ引き継ぎます。そのため `$watch` や `$scan` を挟む循環(要素が `x` へ書き、`$watch: { x(v) { this.mode = v } }`、一覧が `mode` を読む)も、描画 1 回で 1 段ずつ伸びて打ち切られます。これは `$watch` の上限には掛かりません —— ハンドラの書き込みは、直接別のハンドラを起こすのではなく、描画を挟んで戻ってくるからです。普通の一覧の行の初期同期のように収まる書き戻しは 1 段で止まります。連鎖を引き継ぐので、`$watch` や `$scan` でつないだ有限の連鎖は 100 段を分け合います。47 段で収まる見出しの詰めを、終わったら `$watch` が次の詰めを始める形で 2 本つなぐと収まります(描画 94 回)が、同じように 3 本つなぐと 3 本目の途中で打ち切られます。報告は連鎖ごとに 1 回です。描画しなかったバッチが `$watch` や `$scan` を通して連鎖をさらに引き継いでも、続くバッチは報告せずに描画しません。打ち切った後も、次の外からの書き込みは普通に描画されます。DevTools には `state:render-chain-limit` が届きます。
|
|
2832
|
+
|
|
2833
|
+
循環を閉じる書き込みが microtask から届く形は、この上限の対象外です(上限が無かったときと同じです):値を microtask で出す要素(Lit の `updated()` など)と、`await` の後で書く async の `$renderedCallback` です。こうした書き込みは、有限の非同期のループと見分けられません —— 解決済みの Promise を `await` しながら 1 万回書く `async` 関数も、マクロタスクを 1 回も挟まずに 1 万回描画します。続いた描画の数に上限を設けると、正当なコードまで止めてしまいます。
|
|
2834
|
+
|
|
2779
2835
|
### 値と DOM は巻き戻しません
|
|
2780
2836
|
|
|
2781
2837
|
異常系はすべて「報告して続行」で、適用済みの値を戻すことはありません。これは以下で共通の姿勢です:
|
|
@@ -2783,7 +2839,8 @@ this.$getAll("matrix.*.*", [row]);
|
|
|
2783
2839
|
| 機構 | 上限 | 超過時 |
|
|
2784
2840
|
|---|---|---|
|
|
2785
2841
|
| 因果伝播の hop | 32 | その transaction の未処理レコードのみ quarantine |
|
|
2786
|
-
| `$watch` の書き込み連鎖 | 32 | そのバッチの watch
|
|
2842
|
+
| `$watch` の書き込み連鎖 | 32 | そのバッチの `$scan` / `$watch` の発火と、上限を越えた書き込みが起こす `$stream` の再開をスキップ |
|
|
2843
|
+
| 描画中の書き込みの連鎖 | 100 | そのバッチのバインディング適用をスキップ |
|
|
2787
2844
|
| バインディングの適用失敗 | — | その 1 本のみスキップ |
|
|
2788
2845
|
|
|
2789
2846
|
## 設定
|
|
@@ -3062,7 +3119,7 @@ bootstrapState();
|
|
|
3062
3119
|
| `setterPaths` | setter として定義されたパスの Set |
|
|
3063
3120
|
| `createState(mutability, callback)` | 状態プロキシを作成(`"readonly"` または `"writable"`) |
|
|
3064
3121
|
| `createStateAsync(mutability, callback)` | `createState` の非同期版 |
|
|
3065
|
-
| `setInitialState(state)` | プログラムから状態を設定。初期化前は初期 state を渡します。初期化済みの要素では state 全体を入れ替え、確立済みのバインドを戻る前に新しい state で適用し直します(切断中の要素は再接続したときに適用し直します)。新しい state に無いパスのバインドは、古い表示のまま残さず適用の失敗として報告します。再セットは書き込みではないので、`$watch` のハンドラも `$
|
|
3122
|
+
| `setInitialState(state)` | プログラムから状態を設定。初期化前は初期 state を渡します。初期化済みの要素では state 全体を入れ替え、確立済みのバインドを戻る前に新しい state で適用し直します(切断中の要素は再接続したときに適用し直します)。新しい state に無いパスのバインドは、古い表示のまま残さず適用の失敗として報告します。再セットは書き込みではないので、`$watch` のハンドラも `$renderedCallback` も呼びません。リストは配列の同一性で突き合わせるので、長さが変わったリストは新しい配列で渡してください(push や splice でその場で長さを変えた同じ配列インスタンスでの再セットは非対応です)。読み込み済みのボリューム(`<wcs-state mount="…">` —— データはルートの木へ複製済みなので、ルートのマウントパスの下へ書いてください)、ボリュームやマウント済みコンポーネントのあるツリー、初期化に失敗した要素では throw します —— 失敗した要素は再武装できないので、取り除いて作り直してください |
|
|
3066
3123
|
| `nextVersion()` | バージョン番号をインクリメントして返す |
|
|
3067
3124
|
|
|
3068
3125
|
## アーキテクチャ
|
|
@@ -3192,7 +3249,7 @@ const html = await renderToString(template, {
|
|
|
3192
3249
|
|---------|------|
|
|
3193
3250
|
| **サーバー** | `renderToString()` が happy-dom でテンプレートを実行、`$connectedCallback`(`fetch()` 含む)を実行し、全バインディングを適用、ハイドレーションデータを含む `<wcs-ssr>` 要素付きのレンダリング済み HTML を出力 |
|
|
3194
3251
|
| **クライアント** | `<wcs-state enable-ssr>` が `<wcs-ssr>` の JSON から状態をロード、`$connectedCallback` をスキップ、`hydrateBindings()` が既存の DOM にリアクティビティを接続 |
|
|
3195
|
-
| **フォールバック** |
|
|
3252
|
+
| **フォールバック** | サーバー/クライアントのバージョン不一致時、またはサーバーが描いた `for:` の行が別の `for:` / `if:` / `elseif:` / `else:` ブロックの中にあるとき(そのハイドレーションは既知の制限)、SSR DOM をクリーンアップして `buildBindings()` でフルクライアントサイドレンダリングを実行。どちらも理由を `console.warn` で 1 回知らせる |
|
|
3196
3253
|
|
|
3197
3254
|
### `enable-ssr` の動作
|
|
3198
3255
|
|