@wcstack/state 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -71,6 +71,205 @@ type PathInfoSource =
71
71
  /** ランタイム内部のパス翻訳(mapped な bind-component の外向き伝播)。検査しない */
72
72
  | "internal";
73
73
 
74
+ /**
75
+ * 単一の自己再帰宣言。初版はアンカーと反復サブパスとも「固定プロパティ列の末尾に
76
+ * `.*` がひとつ」の形に限定する(docs/state-recursive-path-impl-plan.md §1-1)。
77
+ *
78
+ * 例: `$recursion = { "nodes.*": "children.*" }`
79
+ * - `anchor` … `"nodes.*"`(深さ 0 のノードパス)
80
+ * - `repeat` … `"children.*"`(1 段深くする相対サブパス)
81
+ * - `recursiveAnchor` … `"nodes.**"`
82
+ * - `anchorList` … `"nodes"`
83
+ * - `repeatList` … `"children"`
84
+ *
85
+ * リスト側の 2 つは宣言時に確定させる(静的側の `RecursionSpec` と同じ構成)。
86
+ * 各所で `lastIndexOf(DELIMITER)` の slice を繰り返すと、綴りの取り違えが分散する。
87
+ */
88
+ interface IRecursionSpec {
89
+ readonly anchor: string;
90
+ readonly repeat: string;
91
+ /** `anchor` の `**` 形(`"nodes.**"`)。オーサリング層のパス解析で使う。 */
92
+ readonly recursiveAnchor: string;
93
+ /** `anchor` のリスト側(`"nodes"` — 末尾の `.*` を落とした形)。 */
94
+ readonly anchorList: string;
95
+ /** `repeat` のリスト側(`"children"`)。 */
96
+ readonly repeatList: string;
97
+ }
98
+ /**
99
+ * 展開済みの再帰 getter 1 本ぶんの素性。生成アクセサに紐づくメタデータで、
100
+ * ランタイムが読むのは深さ(`**` の束縛)と元の宣言(診断の名指し)の 2 つだけ。
101
+ * 具体パスは台帳のキー、`PathInfo` は読む側が intern 済みのものを持つので、ここには
102
+ * 重ねて持たない。
103
+ */
104
+ interface IRecursionAccessor {
105
+ /** 元の宣言(`"nodes.**.total"`) */
106
+ readonly recursivePath: string;
107
+ /** 反復の段数(0 origin) */
108
+ readonly depth: number;
109
+ }
110
+
111
+ /**
112
+ * recursion/registry.ts
113
+ *
114
+ * state 1 つぶんの再帰レジストリ。宣言・`**` getter の定義・展開済みアクセサの台帳を
115
+ * 持ち、「具体パスを読む直前に、その深さのアクセサを生やす」遅延実体化を担う。
116
+ *
117
+ * 遅延であることは実装の**不変条件**である(Phase A の A6/A7)。そのパスを一度でも
118
+ * 読んでから生やしても、`isCacheable` が `wildcardCount > 0` だけでキャッシュ可を返す
119
+ * ため `undefined` が `dirty:false` で固定され、以後どう書いても回復しない。
120
+ * したがって実体化は `getByAddress` のキャッシュ参照**前**に置く(E5)。
121
+ *
122
+ * 寿命は state の世代と共にする。`_state` の再セットで `getterPaths` / `listPaths` は
123
+ * クリアされるので、レジストリも作り直す(§1-3)。ただし**生やしたアクセサは state
124
+ * オブジェクトの側に残る**ので、同じ state を再セットすると `getStateInfo` がそれを
125
+ * `getterPaths` に復元する。そのとき「もう生えているから何もしない」と早期 return して
126
+ * しまうと `listPaths` の登録だけが抜け落ちるため、生成物は WeakSet で見分けて
127
+ * 登録だけをやり直す。
128
+ */
129
+
130
+ declare class RecursionRegistry {
131
+ readonly spec: IRecursionSpec;
132
+ private readonly _definitions;
133
+ private readonly _accessors;
134
+ /**
135
+ * `recursiveGetterOwning` の記憶。キーは添字を `*` に畳んだ形(`nodes.1.total` と `nodes.2.total`
136
+ * は 1 つ)、値は「その具体パスを展開形(またはその値の内側)として持つ `**` getter」、
137
+ * 無ければ null。
138
+ *
139
+ * 有界である: キーは添字を畳んだワイルドカード形のパス文字列で、`PathInfo` が intern する集合
140
+ * (バインディング・getter・API 引数に綴られたパスと、その展開形)の部分集合にしかならない。
141
+ * intern 済みパスの集合が有界であることは D10 で受け入れ済みなので、ここも同じ上限に収まる。
142
+ * 文字列は WeakSet に入らないので、寿命はレジストリ(= state の世代)と共にする。
143
+ */
144
+ private readonly _ownerByPath;
145
+ /**
146
+ * 書き込みのホットパス(`setByAddress`)向けの記憶。キーは intern 済みの `PathInfo` なので
147
+ * 寿命と上限は PathInfo の intern 集合と同じ(WeakMap)。畳み(split + Number + join)は
148
+ * miss のときだけ払う — 宣言のある state では**アンカー外を含む全書き込み**がここを通る
149
+ * (第 4 サイクルで実測: 畳みを毎回払うと `s.counter = i` で +100ns/書き込み)。
150
+ */
151
+ private readonly _ownerByPathInfo;
152
+ /**
153
+ * 読みのホットパス(`getByAddress`)向けの記憶。`_ownerByPathInfo` と対称で、キーは
154
+ * intern 済みの `PathInfo`、値は「そのパスの展開アクセサ」、展開形でなければ null。
155
+ * 宣言のある state では**アンカー外を含む全読み**(親ウォークの各段を含む)がここを
156
+ * 通るので、文字列キーの `Map.get` + `Set.has` + `startsWith` を毎回払わせない
157
+ * (第 5 サイクルで実測)。
158
+ *
159
+ * 読みの否定判定の記憶は**ここ 1 つ**(第 5 サイクル再検証で文字列キーの `_nonAccessors` を撤去 —
160
+ * 前段にこの記憶を置いた後は、PathInfo とパス文字列が 1:1 なので二重に持つだけだった)。
161
+ * 否定を記憶してよい根拠は、定義集合が state の世代内で不変であること —
162
+ * 同じ `PathInfo` は同じパス文字列なので、いちど「展開形でない」と決まった PathInfo が
163
+ * 後から実体化されることはない。実体化した側は `materializeForPathInfo` が
164
+ * `_define` の戻り値でそのまま記憶を更新する(否定が実体化を隠さない)。
165
+ */
166
+ private readonly _accessorByPathInfo;
167
+ /** `concretePathAt` の記憶(接尾辞 → 深さ順の具体パス)。 */
168
+ private readonly _concreteBySuffix;
169
+ private readonly _registeredListPaths;
170
+ constructor(spec: IRecursionSpec, state: object);
171
+ /**
172
+ * 作者が手で書いた具体パス(`get "nodes.*.children.*.total"()` / データプロパティ)が、宣言済み
173
+ * `**` getter の展開形と同名でないことを**構築時に**確かめる。
174
+ *
175
+ * `_define` の衝突検査は「その深さを最初に読んだとき」にしか走らないので、データが浅い間は
176
+ * 通り、木が 1 段深くなった瞬間にバインディングが落ちていた(第 3 サイクルのレビューで実測)。
177
+ * 前世代の生成物(own に残った生成 getter)は衝突ではない — 同じ state の再セットで必ず居る。
178
+ */
179
+ private _assertNoConcreteCollision;
180
+ /**
181
+ * 2 本の `**` getter が同じ具体パスへ展開しないことを、宣言だけから静的に確かめる。
182
+ *
183
+ * 衝突するのは「片方の接尾辞がもう片方の接尾辞の末尾で、差分が反復語の整数倍」の
184
+ * ときだけ(`nodes.**.total` と `nodes.**.children.*.total` は深さ k と k+1 で
185
+ * 同じ `nodes.*.children.*.total` になる)。検出しないと `_definitions` の挿入順で
186
+ * 最初に一致した方が無言で勝つ。
187
+ */
188
+ private _assertNoColliding;
189
+ /**
190
+ * `**` getter を 1 本でも宣言しているか。
191
+ * **テスト・診断専用**(ランタイムの経路は `_definitions.size` を直接見る)。
192
+ */
193
+ get hasDefinitions(): boolean;
194
+ /**
195
+ * その接尾辞が宣言済みの `**` getter と衝突するなら、その getter のパスを返す。
196
+ *
197
+ * 完全一致だけでは足りない。①反復語の整数倍だけ違う接尾辞は同じ族を指す
198
+ * (`_assertNoColliding` が宣言どうしについて既に見ている条件)②getter の**下**を
199
+ * 指す形(`nodes.**.total.x` / 反復語ぶんずれた `nodes.**.children.*.total.x`)は、
200
+ * getter が返したオブジェクトへ書いてキャッシュを汚し、次の無効化で無言に戻る。
201
+ * どちらも書き込みの入口(列挙より前)で止める — 述語は expand.ts の `coversSuffix`。
202
+ */
203
+ conflictingRecursiveGetter(suffix: string): string | null;
204
+ /**
205
+ * `recursiveGetterOwning` の intern 済み `PathInfo` 版(書き込みのホットパス用)。
206
+ * WeakMap の hit なら畳みも照合も払わない。
207
+ */
208
+ recursiveGetterOwningPath(pathInfo: IPathInfo): string | null;
209
+ /**
210
+ * 具体パスを展開形(またはその値の内側)として持つ `**` getter のパス。無ければ null。
211
+ * **実体化はしない。**
212
+ *
213
+ * `conflictingRecursiveGetter` の**具体パス版**で、`**` を経ない 2 つの入口が使う:
214
+ *
215
+ * - バインド確立時のパス存在検査(`checkDeclaredPath`)。あの時点ではまだ生えて
216
+ * いないので、素の存在検査では必ず「解決できない」になる。展開形そのもの
217
+ * (`nodes.*.total`)だけでなく、その値の中を指す形(`nodes.*.stats.count` で
218
+ * `get "nodes.**.stats"()` がオブジェクトを返す)も、通常の getter の下と同じく
219
+ * 評価しないと分からないので黙る側に倒す。
220
+ * - 書き込みの入口(`setByAddress`)。`$setAll("nodes.*.children.*.total", [], v)` や
221
+ * `this["nodes.1.total"] = v` は `**` を含まないので `setAllRecursive` の
222
+ * 読み取り専用検査を通らず、未実体化なら fast path が行オブジェクトへ素の
223
+ * プロパティとして書いてしまう(ノードを汚し、代入値が `dirty:false` で載って
224
+ * 以後 getter が評価されない)。展開形への書き込みは、実体化の前後に関わらず
225
+ * `wcs/recursion-readonly` で止める。
226
+ */
227
+ recursiveGetterOwning(concretePath: string): string | null;
228
+ /** 具体パスが宣言済み `**` getter の展開形そのものなら、その getter のパス。 */
229
+ private _matchExpansion;
230
+ /**
231
+ * `materializeFor` の `PathInfo` 版。**読みのホットパス(`getByAddress`)専用**で、
232
+ * 判定そのものは `materializeFor` に委ね、結果(否定を含む)を PathInfo に記憶する。
233
+ * 書き側の `recursiveGetterOwningPath` と対称。
234
+ */
235
+ materializeForPathInfo(stateElement: IStateElement, pathInfo: IPathInfo): IRecursionAccessor | null;
236
+ /**
237
+ * 具体パスが再帰 getter の展開形なら、そのアクセサを(未登録なら生やして)返す。
238
+ * 該当しなければ null。読みは `materializeForPathInfo` を通るので、ここへ来るのは
239
+ * 記憶が外れたときだけ — 判定は接頭辞 1 回で抜け、ここでは否定を記憶しない(記憶は
240
+ * `materializeForPathInfo` の PathInfo キーの 1 か所)。
241
+ * (`**` getter の無い空レジストリを弾くのは呼び出し側の役目。)
242
+ */
243
+ materializeFor(stateElement: IStateElement, concretePath: string): IRecursionAccessor | null;
244
+ private _define;
245
+ /**
246
+ * 経路上のリストパスを `listPaths` に載せる(E4)。`setPathInfo(path, "for")` は
247
+ * 使えない — あちらは `elementPaths` にも入れて `setByAddress` の swap 経路
248
+ * (`isSwappable`)を変えてしまう。ここで要るのは「依存ウォークがこのパスを
249
+ * リストとして展開する」ことだけ。
250
+ */
251
+ private _registerListPaths;
252
+ /**
253
+ * この世代が生やしたもの(own の生成アクセサ・依存辺・キャッシュ)を忘れる(state の
254
+ * 再セット時、`getStateInfo` の再収集より**前**に呼ぶ)。実体は generation.ts。
255
+ */
256
+ forgetGenerated(stateElement: IStateElement, previousState: object): void;
257
+ /**
258
+ * `**` 接尾辞の深さ `depth` の具体パス(`concretePathAt` の記憶付き版)。
259
+ * 束縛形の読み(`this["nodes.**.value"]` / 省略形 `$getAll`)は再帰 getter の評価ごとに
260
+ * ここを通るので、深さぶんの文字列連結とワイルドカード数えを毎回やり直さない。
261
+ * 上限は「接尾辞の種類 × 128」で有界(上限超過は `concretePathAt` が throw するので載らない)。
262
+ */
263
+ concretePathAt(suffix: string, depth: number): string;
264
+ /** 展開済みアクセサのメタデータ(深さ解決・診断・テスト用)。 */
265
+ accessorFor(concretePath: string): IRecursionAccessor | null;
266
+ /**
267
+ * これまでに実体化した具体パスの一覧。**テスト専用**(「読んだ深さだけが生える」という
268
+ * 遅延実体化の不変条件を外から確かめる口。ランタイムはどの経路からも呼ばない)。
269
+ */
270
+ get materializedPaths(): ReadonlySet<string>;
271
+ }
272
+
74
273
  declare const setLoopContextSymbol: unique symbol;
75
274
  declare const getByAddressSymbol: unique symbol;
76
275
  declare const hasByAddressSymbol: unique symbol;
@@ -212,6 +411,32 @@ interface IStateElement {
212
411
  readonly hydratedFromSsr?: boolean;
213
412
  /** ボリュームのアクセサ登録(webComponent/volume.ts 専用) */
214
413
  defineTreeAccessor(path: string, descriptor: PropertyDescriptor): void;
414
+ /**
415
+ * リストパスとしてだけ登録する(`listPaths` に足す)。`setPathInfo(path, "for")` は
416
+ * `elementPaths` にも入れて `setByAddress` の swap 経路(`isSwappable`)を変えるので、
417
+ * 「依存ウォークがこのパスをリストとして展開する」ことだけが要る用途には使えない
418
+ * (docs/state-recursive-path-impl-plan.md §3-2 の E4)。
419
+ */
420
+ addListPath(path: string): void;
421
+ /**
422
+ * state オブジェクト自身+プロトタイプチェーンから descriptor を引く(生成物と作者定義の
423
+ * 見分けに使う)。class 構文の getter は prototype に載るので own だけでは足りない。
424
+ */
425
+ findStateDescriptor(path: string): PropertyDescriptor | undefined;
426
+ /**
427
+ * この state に `$recursion` 宣言があるか。偽のとき getByAddress の遅延実体化と
428
+ * get トラップの `**` 解決は boolean 判定 1 個で抜ける(hasMounts と同じ D18 の形)。
429
+ * 読み手は必ずこのゲートを先に見て、真なら `recursionRegistry` を `!` で読む。
430
+ * 必須メンバー(実装は `State` のみ)。`any` 型のテスト用モックがフィールドを持たなくても
431
+ * 通るのは vitest が型検査をしないからで、その場合 `undefined === true` は偽なので再帰の
432
+ * 経路に入らないだけ — 型としては必須。
433
+ */
434
+ readonly hasRecursion: boolean;
435
+ /**
436
+ * 再帰レジストリ(宣言が無ければ null。`hasRecursion === true` なら非 null)。registry.ts は
437
+ * このファイルの IStateElement を参照するが、`import type` どうしなので実行時の循環にはならない。
438
+ */
439
+ readonly recursionRegistry: RecursionRegistry | null;
215
440
  setPathInfo(path: string, bindingType: BindingType, source?: PathInfoSource): void;
216
441
  addStaticDependency(parentPath: string, childPath: string): boolean;
217
442
  addDynamicDependency(fromPath: string, toPath: string): boolean;
@@ -683,6 +908,8 @@ interface WcsStateApi {
683
908
  readonly $7: number;
684
909
  readonly $8: number;
685
910
  readonly $9: number;
911
+ readonly [key: `${string}.**.${string}`]: any;
912
+ readonly [key: `${string}.**`]: any;
686
913
  }
687
914
  /**
688
915
  * state定義オブジェクト内の `this` の型。
@@ -1086,6 +1313,7 @@ declare class State extends HTMLElementBase implements IStateElement {
1086
1313
  private _resolveSetState;
1087
1314
  private _listPaths;
1088
1315
  private _listKeys;
1316
+ private _recursionRegistry;
1089
1317
  private _elementPaths;
1090
1318
  private _getterPaths;
1091
1319
  private _setterPaths;
@@ -1145,6 +1373,8 @@ declare class State extends HTMLElementBase implements IStateElement {
1145
1373
  get connectedCallbackPromise(): Promise<void>;
1146
1374
  get listPaths(): Set<string>;
1147
1375
  get listKeys(): ListKeyMap | null;
1376
+ get hasRecursion(): boolean;
1377
+ get recursionRegistry(): RecursionRegistry | null;
1148
1378
  get watchPaths(): ReadonlySet<string> | null;
1149
1379
  get elementPaths(): Set<string>;
1150
1380
  /**
@@ -1161,6 +1391,8 @@ declare class State extends HTMLElementBase implements IStateElement {
1161
1391
  enableUpdatedCallback(): void;
1162
1392
  /** enable-ssr スナップショットから初期化されたか(D14 — webComponent/volume.ts が読む)。 */
1163
1393
  get hydratedFromSsr(): boolean;
1394
+ addListPath(path: string): void;
1395
+ findStateDescriptor(path: string): PropertyDescriptor | undefined;
1164
1396
  defineTreeAccessor(path: string, descriptor: PropertyDescriptor): void;
1165
1397
  get getterPaths(): Set<string>;
1166
1398
  get setterPaths(): Set<string>;