@wcstack/state 4.6.1 → 4.7.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 +39 -12
- package/README.md +41 -12
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.esm.js +5 -5
- package/dist/split/auto.js +1 -1
- package/dist/split/chunks/chunk-BCL75PFH.js +2 -0
- package/dist/split/chunks/chunk-BCL75PFH.js.map +1 -0
- package/dist/split/chunks/{chunk-JWRQLKOQ.js → chunk-DRSAQE4E.js} +2 -2
- package/dist/split/chunks/{chunk-GBBBRPZR.js → chunk-GQTEYSRZ.js} +2 -2
- package/dist/split/chunks/{chunk-FBBHQU7S.js → chunk-IBGHHABS.js} +2 -2
- package/dist/split/chunks/{chunk-T3FN37DU.js → chunk-P3EXTBXC.js} +2 -2
- package/dist/split/chunks/{chunk-CBMAVRYG.js → chunk-PKEAYPFR.js} +2 -2
- package/dist/split/chunks/chunk-QMOIMPIJ.js +2 -0
- package/dist/split/chunks/chunk-QMOIMPIJ.js.map +1 -0
- package/dist/split/chunks/chunk-T3KW5DZD.js +2 -0
- package/dist/split/chunks/chunk-T3KW5DZD.js.map +1 -0
- package/dist/split/chunks/chunk-UN6ADZKP.js +2 -0
- package/dist/split/chunks/{chunk-RIZH2AQE.js.map → chunk-UN6ADZKP.js.map} +1 -1
- package/dist/split/chunks/chunk-XXVJ6J2N.js +2 -0
- package/dist/split/chunks/chunk-XXVJ6J2N.js.map +1 -0
- package/dist/split/core.d.ts +2 -0
- package/dist/split/core.js +1 -1
- package/dist/split/features/devtools.d.ts +2 -0
- package/dist/split/features/devtools.js +1 -1
- package/dist/split/features/devtools.js.map +1 -1
- package/dist/split/features/diagnostics.d.ts +2 -0
- package/dist/split/features/diagnostics.js +1 -1
- package/dist/split/features/diagnostics.js.map +1 -1
- package/dist/split/features/formats.d.ts +2 -0
- package/dist/split/features/formats.js +1 -1
- package/dist/split/features/formats.js.map +1 -1
- package/dist/split/features/list-keys.d.ts +2 -0
- package/dist/split/features/list-keys.js +1 -1
- package/dist/split/features/list-keys.js.map +1 -1
- package/dist/split/features/native-commands.d.ts +2 -0
- package/dist/split/features/native-commands.js +1 -1
- package/dist/split/features/native-commands.js.map +1 -1
- package/dist/split/features/recursion.d.ts +2 -0
- package/dist/split/features/recursion.js +1 -1
- package/dist/split/features/recursion.js.map +1 -1
- package/dist/split/features/scopes.d.ts +2 -0
- package/dist/split/features/scopes.js +1 -1
- package/dist/split/features/scopes.js.map +1 -1
- package/dist/split/features/ssr.d.ts +2 -0
- package/dist/split/features/ssr.js +1 -1
- package/dist/split/features/ssr.js.map +1 -1
- package/dist/split/features/temporal.d.ts +2 -0
- package/dist/split/features/temporal.js +1 -1
- package/dist/split/features/temporal.js.map +1 -1
- package/package.json +1 -1
- package/dist/split/chunks/chunk-467LYA2V.js +0 -2
- package/dist/split/chunks/chunk-467LYA2V.js.map +0 -1
- package/dist/split/chunks/chunk-RIZH2AQE.js +0 -2
- package/dist/split/chunks/chunk-TNYNJXEC.js +0 -2
- package/dist/split/chunks/chunk-TNYNJXEC.js.map +0 -1
- package/dist/split/chunks/chunk-ULMHOGJ2.js +0 -2
- package/dist/split/chunks/chunk-ULMHOGJ2.js.map +0 -1
- package/dist/split/chunks/chunk-ZJYPRSTO.js +0 -2
- package/dist/split/chunks/chunk-ZJYPRSTO.js.map +0 -1
- /package/dist/split/chunks/{chunk-JWRQLKOQ.js.map → chunk-DRSAQE4E.js.map} +0 -0
- /package/dist/split/chunks/{chunk-GBBBRPZR.js.map → chunk-GQTEYSRZ.js.map} +0 -0
- /package/dist/split/chunks/{chunk-FBBHQU7S.js.map → chunk-IBGHHABS.js.map} +0 -0
- /package/dist/split/chunks/{chunk-T3FN37DU.js.map → chunk-P3EXTBXC.js.map} +0 -0
- /package/dist/split/chunks/{chunk-CBMAVRYG.js.map → chunk-PKEAYPFR.js.map} +0 -0
package/README.ja.md
CHANGED
|
@@ -193,9 +193,9 @@
|
|
|
193
193
|
<script type="importmap">
|
|
194
194
|
{
|
|
195
195
|
"imports": {
|
|
196
|
-
"@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.
|
|
197
|
-
"@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.
|
|
198
|
-
"@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.
|
|
196
|
+
"@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.7.0/dist/split/core.js",
|
|
197
|
+
"@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.7.0/dist/split/features/temporal.js",
|
|
198
|
+
"@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.7.0/dist/split/features/scopes.js"
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
</script>
|
|
@@ -220,12 +220,18 @@
|
|
|
220
220
|
版を固定するか(素のパスは `exports` を読まないので、`dist/split/` 以下のファイル名を書く)、バンドラを
|
|
221
221
|
通します。**`esm.run` からは読まないでください**。`+esm` はエントリごとにサーバー側で再バンドルし、
|
|
222
222
|
共有の core チャンクをそれぞれに取り込むため、エントリごとに別のエンジンを抱えます。機能は core から
|
|
223
|
-
見えないコピーへ install
|
|
224
|
-
|
|
225
|
-
されます。この形の integrity は [docs/sri.ja.md §5.1](../../docs/sri.ja.md#51-wcstackstate-の分割エントリ) を参照。
|
|
223
|
+
見えないコピーへ install されます。素のファイルなら、どのエントリの相対 import も同じチャンクの URL に
|
|
224
|
+
解決され、エンジンは 1 回だけ評価されます。この形の integrity は [docs/sri.ja.md §5.1](../../docs/sri.ja.md#51-wcstackstate-の分割エントリ) を参照。
|
|
226
225
|
`dist/split/chunks/` 以下のファイル名にはコンテンツハッシュが入ります。チャンクのファイルを自分で列挙する
|
|
227
226
|
場合(preload リンク、import map の `integrity`)は、読み込むビルドからファイル名を取ってください。
|
|
228
227
|
|
|
228
|
+
**core とアドオンは 1 つのビルドから読みます。** 4.7 から、`installFeatures` は別のエンジンの
|
|
229
|
+
コピーのアドオン — 別の URL の同じファイル(`esm.run`)や、別の版(タブが古い core を動かしている間に
|
|
230
|
+
その場で置き換えた自前配信のディレクトリ)— を `[wcs/feature-foreign] The add-on "temporal" is from another copy of @wcstack/state`
|
|
231
|
+
で拒みます。4.6 までは受け入れ、アドオンは別のコピーの口を埋めたので、`$watch` は黙って一度も動かず、
|
|
232
|
+
`formats` のフィルタは入っていないと報告されました。`dist/split/features/` のアドオンのファイル名には
|
|
233
|
+
コンテンツハッシュが入りません。自前で配信するなら、URL に版を入れ、配備の後も古いものを残してください。
|
|
234
|
+
|
|
229
235
|
| エントリ | 足されるもの |
|
|
230
236
|
|---|---|
|
|
231
237
|
| `@wcstack/state/core` | バインディングの本体: `data-wcs`・`{{ }}` とコメントバインディング・`for` / `if`・パス getter・イベント・`$command` / `$on`・条件の 10 個のフィルタ(`eq` `ne` `not` `lt` `le` `gt` `ge` `truthy` `falsy` `boolean`)・`bootstrapState`・`installFeatures` |
|
|
@@ -255,7 +261,7 @@
|
|
|
255
261
|
ページが名指したアドオンを、同じビルドから読み込みます。
|
|
256
262
|
|
|
257
263
|
```html
|
|
258
|
-
<script type="module" src="https://cdn.jsdelivr.net/npm/@wcstack/state@4.
|
|
264
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/@wcstack/state@4.7.0/dist/split/auto.js"></script>
|
|
259
265
|
<wcs-state features="scopes diagnostics">
|
|
260
266
|
<script type="module">
|
|
261
267
|
export default {
|
|
@@ -288,6 +294,13 @@
|
|
|
288
294
|
ページは `@wcstack/state/core` を import して `installFeatures` を呼びます。実行時に import するアドオンは
|
|
289
295
|
その `<script>` の `integrity` の外にあり、CSP の下では CDN のホストを許可しておく必要があります。
|
|
290
296
|
|
|
297
|
+
バンドルするページでは、アドオンはすべて `bootstrapState()` の前に入れます — 1 つのルートだけが使うものも。
|
|
298
|
+
state ごとにアドオンを読み込むのは分割版の auto エントリの仕事で、ルートに入るときに import したアドオンは、
|
|
299
|
+
ディープリンクの部品が作られた後に届きます。HTML を処理するバンドラ(Vite)は、`<wcs-state>` の中の
|
|
300
|
+
インラインの `<script type="module">` も — router の `<wcs-guard-handler>` の中のもの、`<template>` の中のものも —
|
|
301
|
+
警告なしにバンドルへ取り込みます: state は `setInitialState()` を待ったままになり、何も束ねられません。
|
|
302
|
+
そうしたスクリプトには `vite-ignore` を付ける(Vite はページに残します)か、state を `src=` で読んでください。
|
|
303
|
+
|
|
291
304
|
## 基本的な使い方
|
|
292
305
|
|
|
293
306
|
```html
|
|
@@ -745,7 +758,7 @@ export default {
|
|
|
745
758
|
|
|
746
759
|
**`undefined` は「無意見」** — 展開された state パスが `undefined` に解決される場合(slot オブジェクトでその input を初期化していない場合など)、プロパティ書き込みは**スキップ**され、要素側の既定値がそのまま生きます。実際に使うパスだけ初期化すれば十分で、`<wcs-fetch>` が `method` / `manual` / `body` を宣言していても `usersFetch: { value: null, loading: false }` だけで動きます。明示的にクリアしたい場合は `null` を代入してください(`null` は常に書き込まれます)。(このスキップは spread に限らず、要素の input に値を渡すすべてのプロパティバインディングに適用されます。前にそのパスが値を持っていても同じで、要素には状態が最後に書いた値が残ります。これは wc-bindable の applier プロファイル(0.10.0 の A2)より厳しい規則で、A2 は値の後の `undefined` を書いて要素を初期状態に戻させます。状態はこのプロファイルを宣言しません。ほかの書き手 — React 19 や直接の代入 — は `undefined` を届けるので、wcstack の I/O ノードは `undefined` を「値が無い」として扱います。)**表示の表面は別です:** `textContent` / `innerText` / `innerHTML`・mustache やコメントバインディングのテキスト・`attr.*`・`style.*`・`class.*` には生かすべき要素側の既定値が無いので、`undefined` も `null` も「値が無い」— テキストは空になり、属性やスタイルは削除され、クラスは外れます。`class.*` だけは範囲が狭く、「値が無い」扱いになるのは `undefined` と `null` だけです — **それ以外の非 boolean は今も throw します**。クラスの束縛は真偽の切り替えなので、truthy な文字列は意図というより書き間違いである方がずっと多いためです。「truthy として扱う」つもりなら `class.on: flag|truthy` と書いてください。
|
|
747
760
|
|
|
748
|
-
**フィルタを挟んでも契約は保たれます。** 空値の判定はフィルタ**後**の値に対して行われるため、`String(value)` で結果を組み立てるフィルタは、値が無いときに `"undefined"` という**文字**を作ってしまいかねません。書式の族(`upper` / `lower` / `capitalize` / `trim` / `slice` / `padStart` / `padEnd` / `repeat` / `reverse` / `truncate` / `unit`)は代わりに `undefined` / `null` を素通しするので、`attr.title: x|trim` はフィルタの無い `attr.title: x`
|
|
761
|
+
**フィルタを挟んでも契約は保たれます。** 空値の判定はフィルタ**後**の値に対して行われるため、`String(value)` で結果を組み立てるフィルタは、値が無いときに `"undefined"` という**文字**を作ってしまいかねません。書式の族(`upper` / `lower` / `capitalize` / `trim` / `slice` / `padStart` / `padEnd` / `repeat` / `reverse` / `truncate` / `unit`)は代わりに `undefined` / `null` を素通しするので、`attr.title: x|trim` はフィルタの無い `attr.title: x` と同じく属性を消します。4.7 から、日付の族(`date` / `time` / `datetime` / `ymd` / `hms`)も素通しします: 後から届く日付(fetch、ルート)は、届くまで何も描きません — 4.6 までは読み込むたびに拒否され、無い日付の代わりになる `coalesce` の値もありませんでした。次の 3 群は**意図的に素通ししません**: 空値そのものが入力として意味を持つフィルタ(`defaults` / `coalesce` / `nullIfEmpty` / `boolean` / `truthy` / `falsy` / `not` / `eq` / `ne`)、変換が仕事のフィルタ(`int` / `float` / `number` / `string` — `undefined|string` は今も `"undefined"`)、値の型を検査するフィルタ(数値・配列の族。`toFixed` / `join` など)。最後の群は throw しますが、それは束縛 1 本に閉じ込められ `$errorCallback` に報告されます。値が無いことが正当にありうるなら前に `coalesce` を置いてください(`price|coalesce(0)|toFixed(2)`)。
|
|
749
762
|
|
|
750
763
|
**制約事項**:
|
|
751
764
|
|
|
@@ -1757,7 +1770,7 @@ export default {
|
|
|
1757
1770
|
| `ymd(sep?)` | YYYY-MM-DD(既定は `-`) | `createdAt\|ymd` / `createdAt\|ymd(/)` |
|
|
1758
1771
|
| `hms(sep?)` | HH:MM:SS(既定は `:`) | `createdAt\|hms` / `createdAt\|hms(-)` |
|
|
1759
1772
|
|
|
1760
|
-
**5 つとも `Date` を取ります**(タイムスタンプではありません)。他の型は「requires a date value
|
|
1773
|
+
**5 つとも `Date` を取ります**(タイムスタンプではありません)。他の型は「requires a date value」で拒否されます。値が無いとき(`undefined` / `null`)は、4.7 から何も描きません。JSON から読み込んだ state は数値か ISO 文字列を持つので、束縛ではなく読み出し側で変換してください: `get createdAt() { return new Date(this.createdAtMs); }`。`loc?` の既定はページのロケール([ロケール](#ロケール))で、**適用のたびに**読み直されるため、束縛を組み立てた後に確定したロケールも反映されます(明示引数 `date(ja-JP)` は束縛式の一部なので組み立て時に固定されます)。`style?` は `full`・`long`・`medium`・`short`(Intl の `dateStyle` / `timeStyle`)で、無ければロケールの数字の書式(`2026/10/13`)、書式だけならページのロケールです(`date(medium)`)。`currency` の通貨は束縛を組み立てるときに確かめ、数は `loc` が無ければページのロケールで書きます。複数形などほかの書式は getter で書きます(`Intl.PluralRules`、`Intl.NumberFormat`)。
|
|
1761
1774
|
|
|
1762
1775
|
### 真偽値 / デフォルト
|
|
1763
1776
|
|
|
@@ -2607,7 +2620,7 @@ $renderedCallback(paths) {
|
|
|
2607
2620
|
- **行 watch も headless です** —— `for:` も `$listKeys` も要りません: watch は、自分が範囲とするリストを自分で追従させます。同じ配列やその写しの代入と `$postUpdate("items")` では発火しません —— どの行もリストに入っておらず、どの行パスも書かれていないためです —— が、行を読む getter(`$getAll("items.*.price", [])`)はその場で変えた値を読みます。
|
|
2608
2621
|
- **共有配列は位置ごとに発火します** —— 複数の外側の行が同じ配列を持っているとき、その要素の 1 つへの書き込みは、その配列を持つ外側の行ごとに 1 回ずつ、それぞれの添字で `$watch("groups.*.items.*")` を発火させます: それらのどのパスでも値が変わったからです。
|
|
2609
2622
|
- **ハンドラの例外は隔離されます** —— throw は報告され(`$watch "…" failed; the other watches still run.`)、残りの watch(と stream の restart)は続行します。
|
|
2610
|
-
- **書き込みの連鎖には上限があります** —— ハンドラの書き込みは新しいバッチを作るため、相互に書き合う watch は無限ループになり得ます。書き込みはそれぞれ連鎖の深さを持ちます: その書き込みをしたハンドラを起こした書き込みの深さに 1 を足したものです。32 段より深くで起こされたハンドラは実行されません:
|
|
2623
|
+
- **書き込みの連鎖には上限があります** —— ハンドラの書き込みは新しいバッチを作るため、相互に書き合う watch は無限ループになり得ます。書き込みはそれぞれ連鎖の深さを持ちます: その書き込みをしたハンドラを起こした書き込みの深さに 1 を足したものです。32 段より深くで起こされたハンドラは実行されません: 連鎖は断ち切られ、エラーとして([本番でのエラーの報告](#本番でのエラーの報告)。`$watch handlers / $stream restarts kept writing for 32 batches; the chain is cut (nothing is rolled back).`)と DevTools の `state:watch-chain-limit` で 1 回報告され、そのバッチのほかのハンドラは実行されます。値と DOM は巻き戻しません。同じバッチに載る書き込み —— 描画の書き戻し —— は連鎖を伸ばしもリセットもしないので、長い有限の描画の連鎖(`$renderedCallback` が描画ごとに見出しを 1 段ずつ詰める)を見るハンドラは打ち切られません。`$stream` の restart もハンドラと同じく数えます: restart の書き込み(`initial` への戻し・status)は `args` に届いた書き込みの連鎖の続きで、上限を越えた restart は行いません。実行が(再)開始と同じタスクの中で書くもの —— source が出した値、status の `done` / `error` —— はその実行の連鎖に数え、それが起こすハンドラも restart も連鎖の続きになります。そのため、すぐに値を出す source を通るループはページを固まらせずに打ち切られます: `args` で互いの値を読む 2 つの stream も、stream の値を見る `$watch` がその `args` の読むものを進める形も。後者の 1 周(ハンドラ → restart)は 2 段なので、有限かどうかに関わらず 16 周ほどで止まります(メモリ上の source を使う自動ページ送りも)。後のタスク(ネットワークの応答、メッセージ、タイマー)から来た値と、それが起こすハンドラは、数え直しになります。
|
|
2611
2624
|
- **マウントされた `bind-component` スコープでは実行されません** —— マウントされたコンポーネントの `$watch` 宣言は、ルート state へ誘導する `wcs/mount-dollar-declaration` の警告を 1 回出して無視されます。`$stream` と `$renderedCallback` も同様です。ボリュームは `$watch` を拒否します([`mount=`](#追加の状態をマウントするmount)): ルートにフルパス(`"cart.total"`)で宣言してください。plain な(配線なし Shadow の)子は独立ツリーを持つので宣言できます。
|
|
2612
2625
|
- **SSR では実行されません** —— ハンドラの副作用がサーバーとクライアントで二重に走るためです。
|
|
2613
2626
|
|
|
@@ -3083,7 +3096,7 @@ this.$getAll("matrix.*.*", [row]);
|
|
|
3083
3096
|
|
|
3084
3097
|
### 初期化の失敗
|
|
3085
3098
|
|
|
3086
|
-
初期化に失敗した `<wcs-state>` は
|
|
3099
|
+
初期化に失敗した `<wcs-state>` は 1 回報告されます([本番でのエラーの報告](#本番でのエラーの報告))—— まず要素とその state の出どころ、続いてエラー:
|
|
3087
3100
|
|
|
3088
3101
|
```
|
|
3089
3102
|
[@wcstack/state] <wcs-state src="./state.js"> failed to initialize. SyntaxError: …
|
|
@@ -3117,7 +3130,7 @@ this.$getAll("matrix.*.*", [row]);
|
|
|
3117
3130
|
|
|
3118
3131
|
### バインディング 1 本の失敗は 1 本に閉じ込められます
|
|
3119
3132
|
|
|
3120
|
-
バインディングの適用が throw しても、そのバッチの残り・`$renderedCallback`・`$watch`・`$stream` の restart はすべて続行します。失敗は握り潰されず、`$errorCallback`
|
|
3133
|
+
バインディングの適用が throw しても、そのバッチの残り・`$renderedCallback`・`$watch`・`$stream` の restart はすべて続行します。失敗は握り潰されず、`$errorCallback` に、それが無ければ報告され([本番でのエラーの報告](#本番でのエラーの報告))、エラーを続けて出し、DevTools(`state:binding-apply-error`)にも届きます。
|
|
3121
3134
|
|
|
3122
3135
|
```
|
|
3123
3136
|
[@wcstack/state] binding "text: items.*.label" failed to apply.
|
|
@@ -3125,6 +3138,20 @@ this.$getAll("matrix.*.*", [row]);
|
|
|
3125
3138
|
|
|
3126
3139
|
同期や描画に失敗したリスト(キーが無い、getter が throw した)も、その `for` バインディングの失敗として同じように報告され、それまでの行を保ちます。
|
|
3127
3140
|
|
|
3141
|
+
### 本番でのエラーの報告
|
|
3142
|
+
|
|
3143
|
+
4.7 から、エンジンが報告するエラーはすべてプラットフォームの `reportError()` を通ります: 初期化に失敗した `<wcs-state>`、適用に失敗したバインディング(`$errorCallback` が受けないとき)、ページの getter・フィルタ・イベントハンドラ・ライフサイクルのコールバック・`$watch` のハンドラ・トークンの購読者の throw、そしてエンジンが打ち切った更新のループ。ページのエラーハンドラはどれも捕まえられなかったエラーとして受け取り、コンソールには 1 回出ます。4.6 までは `console.error` にだけ出たので、`window.onerror` で作ったエラーの収集には何も届きませんでした — ページ全体の初期化が失敗したときでさえ。
|
|
3144
|
+
|
|
3145
|
+
報告される `Error` は、どこで何が起きたかを言います: メッセージはエンジンの文面(要素、バインディング)に元のエラーを続けたもので、元のエラーは `cause` に入ります。`diagnostics` アドオンが無いと、エンジンの文面はメッセージ番号と値です(`#12 "prop" "count" "textContent" 1 TypeError: …`、`#49 "wcs-state" Error: … #501 "uc"`)— [エラーメッセージと番号](#エラーメッセージと番号) で引けます。
|
|
3146
|
+
|
|
3147
|
+
```js
|
|
3148
|
+
window.addEventListener("error", (e) => {
|
|
3149
|
+
navigator.sendBeacon("/api/client-errors", JSON.stringify({ message: e.message, stack: e.error?.stack, cause: String(e.error?.cause ?? "") }));
|
|
3150
|
+
});
|
|
3151
|
+
```
|
|
3152
|
+
|
|
3153
|
+
`$errorCallback` は、これまでどおり state のバインディングの失敗を代わりに受けます(描く、自分で報告する)。`reportError()` の無い環境 — happy-dom、Node、`@wcstack/server` — では、これまでどおり `console.error` です。警告は `console.warn` のままです。
|
|
3154
|
+
|
|
3128
3155
|
### 描画中の書き込みは無限ループになりません
|
|
3129
3156
|
|
|
3130
3157
|
描画そのものが書き込みを送り返すことがあります: 要素の書き戻し(双方向の入力欄、wc-bindable のイベント —— バインディングが要素のプロパティを設定したときに要素がその場で同期に出すものも含む ——、出力専用メンバーの初期値)と、`$renderedCallback` の中での書き込み(その中の `await` の後も、決着するまで)、`$watch` ハンドラの中での書き込み、`$stream` の restart による書き込みです。こうした書き込みはそれぞれ新しいバッチを作り、そのバッチがまた描画されます。描画が毎回新しい値を書く形 —— たとえば行の要素の出力を、その一覧の getter が読むキーそのものへ束ね、要素がインスタンスごとに違う値を返す —— では、microtask が回り続けてページが固まります。描画だけが送り込んだバッチが続いた数を数え、100 を超えると次のバッチは描画せず(値は state に残ります)、そのバッチに載っていたパスを 1 回だけ報告します:
|
package/README.md
CHANGED
|
@@ -194,9 +194,9 @@ leaves features out, it can compose them instead:
|
|
|
194
194
|
<script type="importmap">
|
|
195
195
|
{
|
|
196
196
|
"imports": {
|
|
197
|
-
"@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.
|
|
198
|
-
"@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.
|
|
199
|
-
"@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.
|
|
197
|
+
"@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.7.0/dist/split/core.js",
|
|
198
|
+
"@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.7.0/dist/split/features/temporal.js",
|
|
199
|
+
"@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.7.0/dist/split/features/scopes.js"
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
</script>
|
|
@@ -221,12 +221,19 @@ Load the split form from the package's own files — jsDelivr's plain, version-p
|
|
|
221
221
|
above (it does not read `exports`, so name the file under `dist/split/`), or a bundler — and **never
|
|
222
222
|
through `esm.run`**. Its `+esm` endpoint re-bundles every entry on the server and inlines the shared
|
|
223
223
|
core chunk into each one, so every entry would carry its own engine: a feature would install into a
|
|
224
|
-
copy the core never sees
|
|
225
|
-
|
|
226
|
-
so the browser evaluates the engine once. Integrity for this form: [docs/sri.md §5.1](../../docs/sri.md#51-the-split-entries-of-wcstackstate).
|
|
224
|
+
copy the core never sees. From the plain files, every entry's relative import resolves to the same
|
|
225
|
+
chunk URL, so the browser evaluates the engine once. Integrity for this form: [docs/sri.md §5.1](../../docs/sri.md#51-the-split-entries-of-wcstackstate).
|
|
227
226
|
The file names under `dist/split/chunks/` carry a content hash: if you list chunk files yourself
|
|
228
227
|
(preload links, `integrity` in an import map), take the names from the build you load.
|
|
229
228
|
|
|
229
|
+
**The core and its add-ons come from one build.** Since 4.7 `installFeatures` refuses an
|
|
230
|
+
add-on of another copy of the engine — the same files under another URL (`esm.run`), or another version
|
|
231
|
+
(a self-hosted directory replaced in place while a tab still runs the old core) — with
|
|
232
|
+
`[wcs/feature-foreign] The add-on "temporal" is from another copy of @wcstack/state`. Up to 4.6 it
|
|
233
|
+
accepted it: the add-on filled the other copy's slots, so a `$watch` never ran, without a word, and a
|
|
234
|
+
filter of `formats` was reported missing. The add-on files under `dist/split/features/` are not
|
|
235
|
+
content-hashed: self-hosted, put the version in their URL and keep the old ones after a deploy.
|
|
236
|
+
|
|
230
237
|
| Entry | What it adds |
|
|
231
238
|
|---|---|
|
|
232
239
|
| `@wcstack/state/core` | The binding engine: `data-wcs`, `{{ }}` and comment bindings, `for` / `if`, path getters, events, `$command` / `$on`, the 10 condition filters (`eq` `ne` `not` `lt` `le` `gt` `ge` `truthy` `falsy` `boolean`), `bootstrapState`, `installFeatures` |
|
|
@@ -256,7 +263,7 @@ receptacle the owning add-on fills on install).
|
|
|
256
263
|
core, and the add-ons the page names from the same build.
|
|
257
264
|
|
|
258
265
|
```html
|
|
259
|
-
<script type="module" src="https://cdn.jsdelivr.net/npm/@wcstack/state@4.
|
|
266
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/@wcstack/state@4.7.0/dist/split/auto.js"></script>
|
|
260
267
|
<wcs-state features="scopes diagnostics">
|
|
261
268
|
<script type="module">
|
|
262
269
|
export default {
|
|
@@ -289,6 +296,14 @@ reason above. It is not in `exports`: a bundler cannot follow the URLs it builds
|
|
|
289
296
|
bundled page imports `@wcstack/state/core` and calls `installFeatures` instead. The add-ons it imports
|
|
290
297
|
at run time are outside the `integrity` of its `<script>`; under a CSP they need the CDN host allowed.
|
|
291
298
|
|
|
299
|
+
In a bundled page, install every add-on before `bootstrapState()` — also one only a route needs: loading
|
|
300
|
+
add-ons per state is what the split auto entry does, and an add-on imported when a route is entered
|
|
301
|
+
arrives after a deep link's components were built. A bundler that processes the HTML (Vite) also takes
|
|
302
|
+
the inline `<script type="module">` out of `<wcs-state>` — and out of the router's `<wcs-guard-handler>`,
|
|
303
|
+
even inside a `<template>` — into its bundle, without a warning: the state then waits for
|
|
304
|
+
`setInitialState()` and nothing binds. Mark such scripts `vite-ignore` (Vite keeps them in the page), or load
|
|
305
|
+
the state with `src=`.
|
|
306
|
+
|
|
292
307
|
## Basic Usage
|
|
293
308
|
|
|
294
309
|
```html
|
|
@@ -751,7 +766,7 @@ Runtime reads `customClass.wcBindable.properties + inputs` and expands each name
|
|
|
751
766
|
|
|
752
767
|
**`undefined` is "no opinion"** — when an expanded state path resolves to `undefined` (e.g. the slot object doesn't initialize that input), the property write is **skipped** and the element keeps its own default. You only need to initialize the paths you actually use; `usersFetch: { value: null, loading: false }` is enough even though `<wcs-fetch>` also declares `method` / `manual` / `body`. To explicitly clear a value, assign `null` — `null` is always written. (This skip applies to every property binding that feeds an element input, not just spread, and also when the path held a value before: the element keeps the last value state wrote. That is stricter than the wc-bindable applier profile (0.10.0, A2), which writes `undefined` after a value so the element returns to its initial state; state does not claim that profile. Other writers — React 19, a direct assignment — do deliver `undefined`, and the wcstack I/O nodes treat it as "no value supplied".) **Display surfaces are different:** `textContent` / `innerText` / `innerHTML`, mustache and comment-binding text, `attr.*`, `style.*` and `class.*` have no element default worth keeping, so `undefined` and `null` both mean "no value" there — the text becomes empty, the attribute or style is removed, and the class is taken off. `class.*` is the narrow case: only `undefined` and `null` mean "no value"; **any other non-boolean still throws**, because a class binding is a boolean toggle and a truthy string is far more likely a mistake than an intent. Write `class.on: flag|truthy` when you mean "treat it as truthy".
|
|
753
768
|
|
|
754
|
-
**Filters keep the contract.** The emptiness test runs on the value *after* the filters, so a filter that built its result with `String(value)` would turn an absent value back into the characters `"undefined"`. The formatting family (`upper`, `lower`, `capitalize`, `trim`, `slice`, `padStart`, `padEnd`, `repeat`, `reverse`, `truncate`, `unit`) passes `undefined` / `null` straight through instead, so `attr.title: x|trim` removes the attribute just as `attr.title: x` does. Three groups deliberately do **not**: the filters for which an absent value *is* the input (`defaults`, `coalesce`, `nullIfEmpty`, `boolean`, `truthy`, `falsy`, `not`, `eq`, `ne`), the conversions whose whole job is to convert (`int`, `float`, `number`, `string` — `undefined|string` is still `"undefined"`), and the filters that check the value's type (the number
|
|
769
|
+
**Filters keep the contract.** The emptiness test runs on the value *after* the filters, so a filter that built its result with `String(value)` would turn an absent value back into the characters `"undefined"`. The formatting family (`upper`, `lower`, `capitalize`, `trim`, `slice`, `padStart`, `padEnd`, `repeat`, `reverse`, `truncate`, `unit`) passes `undefined` / `null` straight through instead, so `attr.title: x|trim` removes the attribute just as `attr.title: x` does. Since 4.7 the date family (`date`, `time`, `datetime`, `ymd`, `hms`) passes them through too: a date that arrives later (a fetch, a route) renders nothing until it is there — up to 4.6 it was rejected on every load, and no `coalesce` value could stand for a missing date. Three groups deliberately do **not**: the filters for which an absent value *is* the input (`defaults`, `coalesce`, `nullIfEmpty`, `boolean`, `truthy`, `falsy`, `not`, `eq`, `ne`), the conversions whose whole job is to convert (`int`, `float`, `number`, `string` — `undefined|string` is still `"undefined"`), and the filters that check the value's type (the number and array families, e.g. `toFixed` / `join`). Those last ones throw, which is confined to the one binding and reported through `$errorCallback` — put a `coalesce` in front when the value can legitimately be absent (`price|coalesce(0)|toFixed(2)`).
|
|
755
770
|
|
|
756
771
|
**Constraints**:
|
|
757
772
|
|
|
@@ -1763,7 +1778,7 @@ The names 3.x kept as aliases of the 3.2 renames (`inc`, `dec`, `fix`, `uc`, `lc
|
|
|
1763
1778
|
| `ymd(sep?)` | YYYY-MM-DD (default `-`) | `createdAt\|ymd` / `createdAt\|ymd(/)` |
|
|
1764
1779
|
| `hms(sep?)` | HH:MM:SS (default `:`) | `createdAt\|hms` / `createdAt\|hms(-)` |
|
|
1765
1780
|
|
|
1766
|
-
**All five take a `Date`,** not a timestamp — a value of any other type is rejected with "requires a date value". State loaded from JSON carries a number or an ISO string, so convert it where it is read rather than in the binding: `get createdAt() { return new Date(this.createdAtMs); }`. `loc?` defaults to the page locale ([Locale](#locale)), and the default is re-read on **every** apply, so a locale settled after the bindings were planned still takes effect (an explicit `date(ja-JP)` is part of the binding expression and is fixed when it is planned). `style?` is `full`, `long`, `medium` or `short` (Intl's `dateStyle` / `timeStyle`); without it the format is the locale's numeric one (`10/13/2026`), and a style alone keeps the page locale (`date(medium)`). `currency`'s code is checked when the bindings are planned; the number is formatted in the page locale unless `loc` names one. Plurals and other formats belong in a getter (`Intl.PluralRules`, `Intl.NumberFormat`).
|
|
1781
|
+
**All five take a `Date`,** not a timestamp — a value of any other type is rejected with "requires a date value". An absent one (`undefined` / `null`) renders nothing since 4.7. State loaded from JSON carries a number or an ISO string, so convert it where it is read rather than in the binding: `get createdAt() { return new Date(this.createdAtMs); }`. `loc?` defaults to the page locale ([Locale](#locale)), and the default is re-read on **every** apply, so a locale settled after the bindings were planned still takes effect (an explicit `date(ja-JP)` is part of the binding expression and is fixed when it is planned). `style?` is `full`, `long`, `medium` or `short` (Intl's `dateStyle` / `timeStyle`); without it the format is the locale's numeric one (`10/13/2026`), and a style alone keeps the page locale (`date(medium)`). `currency`'s code is checked when the bindings are planned; the number is formatted in the page locale unless `loc` names one. Plurals and other formats belong in a getter (`Intl.PluralRules`, `Intl.NumberFormat`).
|
|
1767
1782
|
|
|
1768
1783
|
### Boolean / Default
|
|
1769
1784
|
|
|
@@ -2617,7 +2632,7 @@ Key rules:
|
|
|
2617
2632
|
- **A row watch is headless too** — it needs no `for:` and no `$listKeys`: the watch keeps the lists it ranges over in step itself. Reassigning the same array or a copy of it, and `$postUpdate("items")`, do not fire it — no row entered the list and no row path was written — but a getter over the rows (`$getAll("items.*.price", [])`) sees values changed in place.
|
|
2618
2633
|
- **Shared arrays fire per position** — when several outer rows hold the same array, writing one of its elements fires `$watch("groups.*.items.*")` once for every outer row that holds it, each with its own indexes: the value changed at every one of those paths.
|
|
2619
2634
|
- **Handler exceptions are isolated** — a throw is reported (`$watch "…" failed; the other watches still run.`) and the remaining watches (and stream restarts) still run.
|
|
2620
|
-
- **Write chains are bounded** — a handler's writes form a new batch, so mutually-writing watches would loop forever. Each write carries a chain depth: one more than that of the write that fired the handler making it. A handler fired more than 32 deep does not run: the chain is cut, reported once
|
|
2635
|
+
- **Write chains are bounded** — a handler's writes form a new batch, so mutually-writing watches would loop forever. Each write carries a chain depth: one more than that of the write that fired the handler making it. A handler fired more than 32 deep does not run: the chain is cut, reported once as an error ([Reporting errors in production](#reporting-errors-in-production): `$watch handlers / $stream restarts kept writing for 32 batches; the chain is cut (nothing is rolled back).`) and to DevTools as `state:watch-chain-limit`, while the batch's other handlers still run. Values and DOM are not rolled back. Writes riding in the same batch — a render's write-back — neither lengthen nor reset a chain, so a handler that follows a long finite render chain — `$renderedCallback` shrinking a heading one step per render — is not cut off. A `$stream` restart counts like a handler: its writes (the reset to `initial`, the status) continue the chain of the write that reached its `args`, and a restart past the limit is not made. What a run writes in the same task as its (re)start — a value its source yields, its status `done` / `error` — counts toward that run's chain, for the handlers it fires as for the restarts it causes. A loop through a source that yields at once is therefore cut instead of freezing the page: two streams whose `args` read each other's values, or a `$watch` on a stream's value that moves what its `args` reads. One lap of the latter — the handler, then the restart — is two links, so it stops after about 16 laps, finite or not (auto-paging over an in-memory source included). A value from a later task (a network response, a message, a timer), and the handlers it fires, start afresh.
|
|
2621
2636
|
- **Not run on a mounted `bind-component` scope** — the `$watch` declaration of a mounted component is ignored with a one-time `wcs/mount-dollar-declaration` warning that points to the root state; this applies to `$stream` and `$renderedCallback` too. A volume refuses `$watch` ([`mount=`](#mounting-additional-state-mount)): declare it on the root with the full path (`"cart.total"`). A plain (unwired Shadow) child owns an independent tree and can declare it.
|
|
2622
2637
|
- **SSR does not run watches** — handler side effects would otherwise execute on both server and client.
|
|
2623
2638
|
|
|
@@ -3094,7 +3109,7 @@ this.$getAll("matrix.*.*", [row]);
|
|
|
3094
3109
|
|
|
3095
3110
|
### Initialization failures
|
|
3096
3111
|
|
|
3097
|
-
A `<wcs-state>` that fails to initialize is reported once
|
|
3112
|
+
A `<wcs-state>` that fails to initialize is reported once ([Reporting errors in production](#reporting-errors-in-production)) — first the element and where its state comes from, then the error:
|
|
3098
3113
|
|
|
3099
3114
|
```
|
|
3100
3115
|
[@wcstack/state] <wcs-state src="./state.js"> failed to initialize. SyntaxError: …
|
|
@@ -3128,7 +3143,7 @@ Inside a `for:` / `if:` row, an error found while a binding is attached (an unde
|
|
|
3128
3143
|
|
|
3129
3144
|
### One failing binding is confined to that binding
|
|
3130
3145
|
|
|
3131
|
-
If applying a binding throws, the rest of that batch, `$renderedCallback`, `$watch`, and `$stream` restarts all still run. The failure is not swallowed — it goes to `$errorCallback`, or else
|
|
3146
|
+
If applying a binding throws, the rest of that batch, `$renderedCallback`, `$watch`, and `$stream` restarts all still run. The failure is not swallowed — it goes to `$errorCallback`, or else it is reported ([Reporting errors in production](#reporting-errors-in-production)), followed by the error, and to DevTools (`state:binding-apply-error`):
|
|
3132
3147
|
|
|
3133
3148
|
```
|
|
3134
3149
|
[@wcstack/state] binding "text: items.*.label" failed to apply.
|
|
@@ -3136,6 +3151,20 @@ If applying a binding throws, the rest of that batch, `$renderedCallback`, `$wat
|
|
|
3136
3151
|
|
|
3137
3152
|
A list that fails to sync or render (a missing key, a getter that throws) is reported the same way, as its `for` binding's failure, and keeps the rows it had.
|
|
3138
3153
|
|
|
3154
|
+
### Reporting errors in production
|
|
3155
|
+
|
|
3156
|
+
Since 4.7, every error the engine reports goes through the platform's `reportError()`: a `<wcs-state>` that fails to initialize, a binding that fails to apply (when no `$errorCallback` takes it), and a getter, filter, event handler, lifecycle callback, `$watch` handler or token subscriber of the page that throws — and an update loop it cuts. The page's error handlers see each one as an uncaught error, and the console shows it once. Up to 4.6 they went to `console.error` only, so an error reporter built on `window.onerror` heard nothing — not even when the whole page failed to initialize.
|
|
3157
|
+
|
|
3158
|
+
The reported `Error` says where and what: its message is the engine's (the element, the binding) followed by the original error, which is its `cause`. Without the `diagnostics` add-on the engine's part is the message number and its values (`#12 "prop" "count" "textContent" 1 TypeError: …`, `#49 "wcs-state" Error: … #501 "uc"`) — look them up in [Error messages and numbers](#error-messages-and-numbers).
|
|
3159
|
+
|
|
3160
|
+
```js
|
|
3161
|
+
window.addEventListener("error", (e) => {
|
|
3162
|
+
navigator.sendBeacon("/api/client-errors", JSON.stringify({ message: e.message, stack: e.error?.stack, cause: String(e.error?.cause ?? "") }));
|
|
3163
|
+
});
|
|
3164
|
+
```
|
|
3165
|
+
|
|
3166
|
+
`$errorCallback` still takes a state's binding failures instead (render them, report them yourself). Where there is no `reportError()` — happy-dom, Node, `@wcstack/server` — the report is `console.error`, as before. Warnings stay `console.warn`.
|
|
3167
|
+
|
|
3139
3168
|
### A write made while rendering cannot loop forever
|
|
3140
3169
|
|
|
3141
3170
|
Some writes are fed back by rendering itself: an element's write-back (a two-way input, a wc-bindable event — including one an element dispatches synchronously while a binding sets one of its properties —, an output-only member's initial value), and a write made in `$renderedCallback` (also after an `await` in it, until it settles), in a `$watch` handler, or by a `$stream` restart. Each such write starts a new batch, and that batch renders again. If the rendering writes something new every time — say a row element whose output is bound to the very key the list's getter reads, returning a different value per instance — the page would spin in microtasks and freeze. Batches in a row that only rendering fed are counted: past 100, the next one is not rendered (its values stay in the state), and one report names the paths it carried:
|