@wcstack/state 2.5.1 → 2.6.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 CHANGED
@@ -969,6 +969,7 @@ get total() { return this.price * exchangeRate; } // モジュール変
969
969
  | `this.$trackDependency(path)` | 依存を明示的に追加し、そのパスの変更でこの getter を dirty にする |
970
970
  | `this.$postUpdate(path)` | 追跡外の入力が変わったことを getter の外から通知する |
971
971
  | `this.$untrackDependency(fn)` | 依存として登録せずにパスを読む(上の対称) |
972
+ | `this.$eq(path, key)` / `$eqPath(path, keyPath)` / `$eqIndex(path)` | 鍵付き購読: 「`path` はこの行の鍵に等しいか」をパターン依存なしで答える([鍵付き選択](#鍵付き選択eq--eqpath--eqindex)) |
972
973
 
973
974
  ```javascript
974
975
  // ✅ 時計を state 側で刻み、getter は純粋なまま
@@ -995,6 +996,31 @@ getter の例外は握り潰されません。評価された場所(バイン
995
996
 
996
997
  `$untrackDependency(fn)` は setter の規則を getter に意図的に適用するもので、`fn` の中の読み取りは追跡されません。`$trackDependency(path)` は最初の規則に対する逃げ道です。
997
998
 
999
+ ### 鍵付き選択(`$eq` / `$eqPath` / `$eqIndex`)
1000
+
1001
+ 「この行は選択中か」を答える行 getter は、依存追跡が最も効きにくい形です。`get "items.*.selected"() { return this.$1 === this.selectedIndex; }` と書くと全行が `selectedIndex` に依存し、1 クリックで 1 万行の getter が再評価されます。鍵付きの形は各行を **その行自身の鍵** の下に購読させ、パスへの書き込みでは「選択されていた行」と「選択される行」だけを再評価します:
1002
+
1003
+ | API | 鍵 | 選択の付き先 | 備考 |
1004
+ |---|---|---|---|
1005
+ | `this.$eq(path, key)` | 任意の値 | 鍵 | `path` は依存を張らずに読み、行は `key` の下に登録される |
1006
+ | `this.$eqPath(path, keyPath)` | `keyPath` の値(ワイルドカードはこの行で解決) | id | 鍵も依存を張らずに読むので、リストの置換や並べ替えで行が再評価されない |
1007
+ | `this.$eqIndex(path, level = 1)` | この行の index(`$1`。`level` でワイルドカード段を選ぶ) | index | `$1` を読む場合と違い getter を **index 依存に記録しない**: 行の移動はリスト差分が鍵を付け替えるので、1 行削除で再評価されるのは高々 2 行 |
1008
+
1009
+ ```javascript
1010
+ export default {
1011
+ items: [],
1012
+ selectedId: null,
1013
+ selectedIndex: null,
1014
+ // id による選択: 並べ替えや削除をまたいで同じ行に付いて行く
1015
+ get "items.*.selected"() { return this.$eqPath("selectedId", "items.*.id"); },
1016
+ // 位置による選択: 同じ仕事量で index に付く
1017
+ get "items.*.current"() { return this.$eqIndex("selectedIndex"); },
1018
+ onSelect(e, $1) { this.selectedId = this["items." + $1 + ".id"]; this.selectedIndex = $1; },
1019
+ };
1020
+ ```
1021
+
1022
+ 規則: 3 つの呼び出しが購読を張るのはリスト行の getter の中で評価されたときだけで、それ以外では比較結果を返すだけです。行の購読はリスト差分がその行を外した時点で落ちます。鍵の比較は `Object.is` ですが、Map の意味論により `+0` と `-0`、`NaN` 同士は同じ鍵になります。`$eqPath` は鍵を依存なしで読むので、行の鍵がその場で書き換わっても行は再評価されません — 変わらない同一性(id)に使い、鍵自体が動く場合は追跡付きの読みと `$eq` を組み合わせてください。
1023
+
998
1024
  ### ループインデックス変数(`$1`, `$2`, ...)
999
1025
 
1000
1026
  getter やイベントハンドラ内で、`this.$1`、`this.$2` などで現在のループイテレーションのインデックスを取得できます(0始まりの値、1始まりの命名):
@@ -1041,6 +1067,7 @@ export default {
1041
1067
  | `this.$postUpdate(path)` | 指定パスの更新通知を手動で発行 |
1042
1068
  | `this.$trackDependency(path)` | キャッシュ無効化のための依存関係を手動で登録 |
1043
1069
  | `this.$untrackDependency(fn)` | fn 実行中の依存追跡を抑止して値を読む(`$trackDependency` と対称) |
1070
+ | `this.$eq(path, key)` / `$eqPath(path, keyPath)` / `$eqIndex(path, level?)` | 鍵付き購読: 「`path` の値はこの行の鍵か」([鍵付き選択](#鍵付き選択eq--eqpath--eqindex)) |
1044
1071
  | `this.$stateElement` | `IStateElement` インスタンスへのアクセス |
1045
1072
  | `this.$1`, `this.$2`, ... | 現在のループインデックス(1始まりの命名、0始まりの値) |
1046
1073
 
@@ -2641,6 +2668,38 @@ this.$getAll("matrix.*.*", [row]);
2641
2668
  | `$watch` の書き込み連鎖 | 32 | そのバッチの watch 発火をスキップ |
2642
2669
  | バインディングの適用失敗 | — | その 1 本のみスキップ |
2643
2670
 
2671
+ ### 3.0 への準備(`wcs/v3-migration`)
2672
+
2673
+ 3.0 は互換層を持ちません。代わりにこのリリースは、3.0 が拒否する書き方・読み方が変わる書き方を 2.x のまま動かしつつ名指しで知らせます。警告は書き方とサイトごとに 1 回だけ、コード `wcs/v3-migration` で出し、3.0 での扱いと今の書き換え先を示します:
2674
+
2675
+ ```
2676
+ [@wcstack/state] [wcs/v3-migration] "value#ro#wo": 3.0 rejects a second "#". Write "value#ro,wo".
2677
+ See "Preparing for 3.0" in the @wcstack/state README.
2678
+ ```
2679
+
2680
+ | 書き方 | 2.x | 3.0 | 今の書き方 |
2681
+ |---|---|---|---|
2682
+ | 2 つ目の `#`(`value#ro#wo:`) | 最初の修飾子列だけ残す | `[wcs/binding-syntax]` | `value#ro,wo:` |
2683
+ | `else:` の後ろの値 | 無視 | `[wcs/binding-syntax]` | `else:` |
2684
+ | `for` / `if` / `elseif` / `else` / `...` の修飾子・フィルタ | ただのプロパティのバインディングになる | `[wcs/binding-syntax]` | キーワードだけ |
2685
+ | `radio#ro:` / `checkbox#ro:` | `radio` という名前のプロパティになり効かない | 修飾子を守る radio / checkbox のバインディング | —(意図どおりか確かめる) |
2686
+ | フィルタ引数の閉じていない引用符 | 黙って閉じる | `[wcs/binding-syntax]` | 引用符を閉じる |
2687
+ | フィルタが受け取る数を超える引数 | 余りは無視 | `[wcs/filter-arity]` | 余りを消す |
2688
+ | `eq` / `ne` の引用符の無い `true` / `false` / `null`(`eq(true)`) | 真偽値・`null` の値を文字列と比べる | 型付きの値と比べる | 文字列で比べ続けるなら `eq('true')` |
2689
+ | `defaults` の引用符の無い `true` / `false` / `null` | 文字列(`"null"`)を既定値にする | 型付きの値を既定値にする | 文字列のままなら `defaults('null')` |
2690
+ | `truthy` / `falsy` / `defaults` に来た `0n` | 真 | 偽(JavaScript の真偽判定) | — |
2691
+ | `textContent` / `innerText` / `innerHTML` への `undefined` | 前のテキストを残す(使い回した行では前の行のもの) | 空にする | 「値なし」は `""` か `null` を返す |
2692
+ | `attr.*` への `undefined` / `null` | `"undefined"` / `"null"` を書く | 属性を削除する | — |
2693
+ | `style.*` への `undefined` | 前の値を残す | 消す | — |
2694
+ | `$resolve(path, indexes, undefined)` | 読む | `undefined` を書く(引数の個数で決める) | 読むなら `$resolve(path, indexes)` |
2695
+ | readonly のプロキシからの `$resolve(path, indexes, value)` / `$setAll` | 書く | `This state is readonly.` を投げる | `createState("writable", …)` から書く |
2696
+ | `#ro` のマウント(`state#ro: user`)を通るコンポーネントの書き込み | ホストのツリーに書く | `[wcs/mount-readonly]` | ホストで書くか、`#ro` を外す |
2697
+ | 部分マウントに隠された自前の既定値(コンポーネントが `name` を宣言し、`state.name: user.name`) | 既定値が勝つ(警告あり) | 明示したマウントが勝つ | 既定値を消す |
2698
+
2699
+ もう 1 つは予告でなく先に入れています:ボリュームやマウントされたコンポーネントの `$errorCallback` は黙って無視していましたが、ルート専用のキー(`$commandTokens`、`$on` など)を挙げる既存の警告で名指しするようにしました(3.0 と同じ)。動くのは今もルートの state だけです。
2700
+
2701
+ 値の警告はその値が実際に来たときにだけ出ます。コンソールが静かでも、テストが通らなかった経路については何も分かりません。文法の行はバインディングを最初に解析したときに調べます。`npx @wcstack/lint <file>`(と VS Code 拡張)も同じ判定で `wcs/v3-migration`(info)として報告するので、ページを動かさずに見つけられます。
2702
+
2644
2703
  ## 設定
2645
2704
 
2646
2705
  `bootstrapState()` に部分的な設定オブジェクトを渡します:
package/README.md CHANGED
@@ -969,6 +969,7 @@ The rule: **read only through `this`, and don't write state or touch the DOM fro
969
969
  | `this.$trackDependency(path)` | Register an extra dependency so this getter is dirtied when that path changes |
970
970
  | `this.$postUpdate(path)` | Announce that an untracked input changed, from outside the getter |
971
971
  | `this.$untrackDependency(fn)` | Read a path *without* registering it as a dependency (the inverse) |
972
+ | `this.$eq(path, key)` / `$eqPath(path, keyPath)` / `$eqIndex(path)` | A keyed subscription: "is `path` equal to this row's key?" without a pattern dependency (see [Keyed selection](#keyed-selection-eq--eqpath--eqindex)) |
972
973
 
973
974
  ```javascript
974
975
  // ✅ The clock ticks in state; the getter stays pure
@@ -995,6 +996,31 @@ The first rule is the one static analysis can catch: `wcs-validate` and the VS C
995
996
 
996
997
  `$untrackDependency(fn)` applies the setter rule to a getter on purpose: reads inside `fn` are not tracked. `$trackDependency(path)` is the escape hatch for the first rule.
997
998
 
999
+ ### Keyed selection (`$eq` / `$eqPath` / `$eqIndex`)
1000
+
1001
+ A row getter that answers "is this row the selected one?" is the classic case where dependency tracking scales badly: `get "items.*.selected"() { return this.$1 === this.selectedIndex; }` makes every row depend on `selectedIndex`, so one click re-evaluates 10,000 getters. The keyed forms subscribe each row under **its own key** instead, and a write to the path re-evaluates only the row that was selected and the row that becomes selected:
1002
+
1003
+ | API | Key | Selection follows | Notes |
1004
+ |---|---|---|---|
1005
+ | `this.$eq(path, key)` | any value | the key | `path` is read without a dependency; the row registers under `key` |
1006
+ | `this.$eqPath(path, keyPath)` | the value at `keyPath` (wildcards resolve to this row) | the id | reads the key without a dependency too, so replacing or reordering the list never re-evaluates the rows |
1007
+ | `this.$eqIndex(path, level = 1)` | this row's index (`$1`; `level` picks the wildcard) | the index | unlike reading `$1`, the getter is **not** recorded as index-dependent: the list diff re-keys moved rows, so removing a row re-evaluates at most two rows |
1008
+
1009
+ ```javascript
1010
+ export default {
1011
+ items: [],
1012
+ selectedId: null,
1013
+ selectedIndex: null,
1014
+ // selection by id: survives sorting and removal
1015
+ get "items.*.selected"() { return this.$eqPath("selectedId", "items.*.id"); },
1016
+ // selection by position: the same row count of work, keyed on the index
1017
+ get "items.*.current"() { return this.$eqIndex("selectedIndex"); },
1018
+ onSelect(e, $1) { this.selectedId = this["items." + $1 + ".id"]; this.selectedIndex = $1; },
1019
+ };
1020
+ ```
1021
+
1022
+ Rules: the three calls only subscribe when evaluated inside a getter under a list row (elsewhere they just return the comparison); a row's subscription is dropped when the list diff removes the row; keys compare with `Object.is` except that Map semantics treat `+0`/`-0` and `NaN`/`NaN` as equal. `$eqPath` reads the key without a dependency, so a row whose key changes in place is not re-evaluated by that change — use it for identities that do not change (ids), and `$eq` with a tracked read when the key itself is live.
1023
+
998
1024
  ### Loop Index Variables (`$1`, `$2`, ...)
999
1025
 
1000
1026
  Inside getters and event handlers, `this.$1`, `this.$2`, etc. provide the current loop iteration index (0-based value, 1-based naming):
@@ -1041,6 +1067,7 @@ Inside state objects (getters / methods), the following APIs are available via `
1041
1067
  | `this.$postUpdate(path)` | Manually trigger update notification for a path |
1042
1068
  | `this.$trackDependency(path)` | Manually register a dependency for cache invalidation |
1043
1069
  | `this.$untrackDependency(fn)` | Read values inside fn without registering dependencies (symmetric to `$trackDependency`) |
1070
+ | `this.$eq(path, key)` / `$eqPath(path, keyPath)` / `$eqIndex(path, level?)` | Keyed subscription for "is this row's key the value of `path`?" (see [Keyed selection](#keyed-selection-eq--eqpath--eqindex)) |
1044
1071
  | `this.$command.<name>` | Access a `CommandToken` declared in `$commandTokens` (see [Command Token](#command-token-method-binding)) |
1045
1072
  | `this.$stateElement` | Access to the `IStateElement` instance |
1046
1073
  | `this.$1`, `this.$2`, ... | Current loop index (1-based naming, 0-based value) |
@@ -2645,6 +2672,38 @@ Every failure mode reports and continues; nothing already applied is reverted:
2645
2672
  | `$watch` write chain | 32 | Skip watch firing for that batch |
2646
2673
  | Binding apply failure | — | Skip that one binding |
2647
2674
 
2675
+ ### Preparing for 3.0 (`wcs/v3-migration`)
2676
+
2677
+ 3.0 has no compatibility layer. Instead, this release names each form that 3.0 rejects or reads differently, while still running it the 2.x way. Each warning is printed once per form and site, under the code `wcs/v3-migration`, and says what 3.0 does and what to write now:
2678
+
2679
+ ```
2680
+ [@wcstack/state] [wcs/v3-migration] "value#ro#wo": 3.0 rejects a second "#". Write "value#ro,wo".
2681
+ See "Preparing for 3.0" in the @wcstack/state README.
2682
+ ```
2683
+
2684
+ | Form | 2.x | 3.0 | Write now |
2685
+ |---|---|---|---|
2686
+ | A second `#` (`value#ro#wo:`) | Keeps the first modifier list | `[wcs/binding-syntax]` | `value#ro,wo:` |
2687
+ | A value after `else:` | Ignored | `[wcs/binding-syntax]` | `else:` |
2688
+ | Modifiers or filters on `for` / `if` / `elseif` / `else` / `...` | A plain property binding | `[wcs/binding-syntax]` | The bare keyword |
2689
+ | `radio#ro:` / `checkbox#ro:` | A property named `radio`, no effect | A radio / checkbox binding that honours the modifiers | — (check the binding still does what you meant) |
2690
+ | An unterminated quote in filter arguments | Closed silently | `[wcs/binding-syntax]` | Close the quote |
2691
+ | More filter arguments than the filter takes | The rest ignored | `[wcs/filter-arity]` | Remove them |
2692
+ | Unquoted `true` / `false` / `null` in `eq` / `ne` (`eq(true)`) | Compares a boolean or `null` value with the string | Compares with the typed value | `eq('true')` to keep the text comparison |
2693
+ | Unquoted `true` / `false` / `null` in `defaults` | Falls back to the string (`"null"`) | Falls back to the typed value | `defaults('null')` to keep the text |
2694
+ | `0n` through `truthy` / `falsy` / `defaults` | Truthy | Falsy (JavaScript truthiness) | — |
2695
+ | `undefined` into `textContent` / `innerText` / `innerHTML` | Keeps the previous text (in a reused row, the previous row's) | Empties it | Return `""` or `null` for "no value" |
2696
+ | `undefined` / `null` into `attr.*` | Writes `"undefined"` / `"null"` | Removes the attribute | — |
2697
+ | `undefined` into `style.*` | Keeps the previous value | Clears it | — |
2698
+ | `$resolve(path, indexes, undefined)` | Reads | Writes `undefined` (the argument count decides) | `$resolve(path, indexes)` to read |
2699
+ | `$resolve(path, indexes, value)` / `$setAll` on a readonly proxy | Writes | Throws `This state is readonly.` | Write from `createState("writable", …)` |
2700
+ | A component writing through a `#ro` mount (`state#ro: user`) | Writes the host's tree | `[wcs/mount-readonly]` | Write on the host, or drop `#ro` |
2701
+ | An own default shadowed by a partial mount (`state.name: user.name` while the component declares `name`) | The default wins, with a warning | The explicit mount wins | Remove the default |
2702
+
2703
+ One more change arrives early instead of being announced: `$errorCallback` on a volume or a mounted component, which was ignored silently, is now named by the warning that already lists the root-only keys (`$commandTokens`, `$on`, …), as 3.0 does. It still runs only on the root state.
2704
+
2705
+ A value warning fires only when that value actually arrives, so a clean console proves nothing about the paths your tests did not reach. The syntax rows are checked when a binding is first parsed, and `npx @wcstack/lint <file>` (and the VS Code extension) reports them with the same check, as `wcs/v3-migration` at info severity, without running the page.
2706
+
2648
2707
  ## Configuration
2649
2708
 
2650
2709
  Pass a partial configuration object to `bootstrapState()`: