@wcstack/state 1.24.0 → 1.25.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.esm.js CHANGED
@@ -7102,7 +7102,7 @@ async function buildBindings(root) {
7102
7102
  }
7103
7103
  }
7104
7104
 
7105
- var version = "1.24.0";
7105
+ var version = "1.25.0";
7106
7106
  var pkg = {
7107
7107
  version: version};
7108
7108
 
@@ -9970,6 +9970,108 @@ function setSwapInfoByAddress(address, swapInfo) {
9970
9970
  }
9971
9971
  }
9972
9972
 
9973
+ /**
9974
+ * topologicalRank.ts — 依存グラフ(パス単位)のトポロジカル順位。
9975
+ *
9976
+ * 依存ウォークは list → list.* を展開するために途中でリスト実体を読む。この読み取りが
9977
+ * 正しい値を返すには「そのパスの入力(先行パス)がすべて dirty 化済み」である必要がある。
9978
+ * DFS ではダイヤモンド依存で片腕しか dirty 化していない段階で合流点を評価してしまうため、
9979
+ * パス単位の rank(= 最長経路長)を先に求め、rank の昇順で訪問する。
9980
+ *
9981
+ * rank の定義から、辺 (u → v) が存在すれば必ず rank(u) < rank(v) となる。したがって
9982
+ * rank r のバケットを処理する時点で rank < r のパスはすべて訪問(dirty 化)済みであり、
9983
+ * 同じバケット内のパス同士は互いに先行関係を持たない。
9984
+ *
9985
+ * 値を読まないグラフ走査なので、ウォーク 1 回あたりの追加コストは実測で誤差に
9986
+ * 収まる(メモ化しても差が出なかったため、キャッシュは持たない)。
9987
+ */
9988
+ function getTopologicalRanks(startPath, staticMap, dynamicMap, maxDepth) {
9989
+ // 1) startPath から到達可能なパス部分グラフと入次数を求める(値は一切読まない)
9990
+ const adjacency = new Map();
9991
+ const inDegree = new Map();
9992
+ const pending = [startPath];
9993
+ inDegree.set(startPath, 0);
9994
+ while (pending.length > 0) {
9995
+ const path = pending.pop();
9996
+ if (adjacency.has(path)) {
9997
+ continue;
9998
+ }
9999
+ const staticDeps = staticMap.get(path);
10000
+ const dynamicDeps = dynamicMap.get(path);
10001
+ let deps;
10002
+ if (staticDeps === undefined) {
10003
+ deps = dynamicDeps ?? [];
10004
+ }
10005
+ else if (dynamicDeps === undefined) {
10006
+ deps = staticDeps;
10007
+ }
10008
+ else {
10009
+ deps = staticDeps.concat(dynamicDeps);
10010
+ }
10011
+ adjacency.set(path, deps);
10012
+ for (let i = 0; i < deps.length; i++) {
10013
+ const dep = deps[i];
10014
+ inDegree.set(dep, (inDegree.get(dep) ?? 0) + 1);
10015
+ if (!adjacency.has(dep)) {
10016
+ pending.push(dep);
10017
+ }
10018
+ }
10019
+ }
10020
+ // 2) Kahn 法。rank は最長経路長(rank[v] = max(rank[u]) + 1)。
10021
+ // 入次数が 0 に落ちて queue に入ったパスだけが「確定」で、緩和の途中で
10022
+ // 暫定値が入っただけのパス(= 循環の一部)は確定扱いにしない。
10023
+ const ranks = new Map();
10024
+ const settled = new Set();
10025
+ const queue = [];
10026
+ for (const [path, degree] of inDegree) {
10027
+ if (degree === 0) {
10028
+ ranks.set(path, 0);
10029
+ settled.add(path);
10030
+ queue.push(path);
10031
+ }
10032
+ }
10033
+ for (let i = 0; i < queue.length; i++) {
10034
+ const path = queue[i];
10035
+ const nextRank = ranks.get(path) + 1;
10036
+ if (nextRank > maxDepth) {
10037
+ raiseError(`Maximum dependency depth of ${maxDepth} exceeded. Possible circular dependency detected at path: ${path}`);
10038
+ }
10039
+ const deps = adjacency.get(path);
10040
+ for (let j = 0; j < deps.length; j++) {
10041
+ const dep = deps[j];
10042
+ if (nextRank > (ranks.get(dep) ?? -1)) {
10043
+ ranks.set(dep, nextRank);
10044
+ }
10045
+ const remaining = inDegree.get(dep) - 1;
10046
+ inDegree.set(dep, remaining);
10047
+ if (remaining === 0) {
10048
+ settled.add(dep);
10049
+ queue.push(dep);
10050
+ }
10051
+ }
10052
+ }
10053
+ // 3) 循環に含まれるパスは rank が決まらない(入次数が 0 に落ちない)。
10054
+ // そもそも正しい評価順が存在しないので、順序保証を諦めて確定済みの
10055
+ // 最大 rank の次にまとめる。打ち切りは従来どおり visited が担う。
10056
+ // 暫定値が残っていると確定パスとの前後関係を誤って表すため、必ず上書きする。
10057
+ if (settled.size !== adjacency.size) {
10058
+ let maxRank = -1;
10059
+ for (const path of settled) {
10060
+ const rank = ranks.get(path);
10061
+ if (rank > maxRank) {
10062
+ maxRank = rank;
10063
+ }
10064
+ }
10065
+ const cycleRank = maxRank + 1;
10066
+ for (const path of adjacency.keys()) {
10067
+ if (!settled.has(path)) {
10068
+ ranks.set(path, cycleRank);
10069
+ }
10070
+ }
10071
+ }
10072
+ return ranks;
10073
+ }
10074
+
9973
10075
  const MAX_DEPENDENCY_DEPTH = 1000;
9974
10076
  function getIndexes(listDiff, searchType) {
9975
10077
  switch (searchType) {
@@ -10076,152 +10178,178 @@ function getMovedRowExpansionPaths(context, wildcardPath, depPathInfo) {
10076
10178
  return result ?? EMPTY_PATH_INFOS;
10077
10179
  }
10078
10180
  function _walkDependency(context, startAddress, callback) {
10079
- const stack = [{ address: startAddress, depth: 0 }];
10080
- while (stack.length > 0) {
10081
- const { address, depth } = stack.pop();
10082
- if (depth > MAX_DEPENDENCY_DEPTH) {
10083
- raiseError(`Maximum dependency depth of ${MAX_DEPENDENCY_DEPTH} exceeded. Possible circular dependency detected at path: ${address.pathInfo.path}`);
10084
- }
10085
- if (context.visited.has(address)) {
10181
+ // rank ごとのバケットで訪問する。辺 (u v) では必ず rank(u) < rank(v) なので、
10182
+ // バケット r を処理する時点で rank < r のパスは全て dirty 化済みになる
10183
+ // ここでリスト実体を読んでも入力が揃っている(topologicalRank.ts 参照)。
10184
+ const buckets = [];
10185
+ const ranks = context.ranks;
10186
+ const enqueue = (address, minRank) => {
10187
+ let rank = ranks.get(address.pathInfo.path) ?? minRank;
10188
+ if (rank < minRank) {
10189
+ // 循環など rank が先行関係を表せないケース。現在のバケットに載せて
10190
+ // 同一ループ内で処理する(打ち切りは visited が担う)。
10191
+ rank = minRank;
10192
+ }
10193
+ (buckets[rank] ??= []).push(address);
10194
+ };
10195
+ enqueue(startAddress, 0);
10196
+ // 依存アドレスを収集するための一時バッファ(アドレスごとに使い回す)
10197
+ const nextEntries = [];
10198
+ for (let rank = 0; rank < buckets.length; rank++) {
10199
+ const bucket = buckets[rank];
10200
+ if (bucket === undefined) {
10086
10201
  continue;
10087
10202
  }
10088
- context.visited.add(address);
10089
- callback(address);
10090
- const sourcePath = address.pathInfo.path;
10091
- const nextDepth = depth + 1;
10092
- // 依存アドレスを逆順でpushするための一時バッファ
10093
- const nextEntries = [];
10094
- /**
10095
- * パスから依存関係をたどる
10096
- * users.*.name <= users.* <= users
10097
- * ただし、users がリストであれば users.* の依存関係は展開する
10098
- */
10099
- const staticDeps = context.staticMap.get(sourcePath);
10100
- if (staticDeps) {
10101
- for (const dep of staticDeps) {
10102
- const depPathInfo = getPathInfo(dep);
10103
- if (context.listPathSet.has(sourcePath) && depPathInfo.lastSegment === WILDCARD) {
10104
- //expand indexes
10105
- const newValue = context.stateProxy[getByAddressSymbol](address);
10106
- const absPathInfo = getAbsolutePathInfo(context.stateElement, address.pathInfo);
10107
- const absAddress = createAbsoluteStateAddress(absPathInfo, address.listIndex);
10108
- const lastValue = getLastListValueByAbsoluteStateAddress(absAddress);
10109
- const listDiff = createListDiff(address.listIndex, lastValue, newValue);
10110
- const selection = selectExpansionIndexes(context, sourcePath, lastValue, newValue, listDiff);
10111
- for (const listIndex of selection.fullRows) {
10112
- const depAddress = createStateAddress(depPathInfo, listIndex);
10113
- context.result.add(depAddress);
10114
- nextEntries.push({ address: depAddress, depth: nextDepth });
10203
+ // 同一バケットへの push(循環時)で伸びるため length は都度読む
10204
+ for (let cursor = 0; cursor < bucket.length; cursor++) {
10205
+ const address = bucket[cursor];
10206
+ if (context.visited.has(address)) {
10207
+ continue;
10208
+ }
10209
+ context.visited.add(address);
10210
+ callback(address);
10211
+ nextEntries.length = 0;
10212
+ _collectDependencies(context, address, nextEntries);
10213
+ for (let i = 0; i < nextEntries.length; i++) {
10214
+ enqueue(nextEntries[i], rank + 1);
10215
+ }
10216
+ }
10217
+ }
10218
+ }
10219
+ /**
10220
+ * address の依存アドレスを nextEntries に集め、context.result にも登録する。
10221
+ * リスト展開(list list.*)と動的依存のワイルドカード展開はここで値を読むが、
10222
+ * 呼び出し元がトポロジカル順を保証しているため入力は揃っている。
10223
+ */
10224
+ function _collectDependencies(context, address, nextEntries) {
10225
+ const sourcePath = address.pathInfo.path;
10226
+ /**
10227
+ * パスから依存関係をたどる
10228
+ * users.*.name <= users.* <= users
10229
+ * ただし、users がリストであれば users.* の依存関係は展開する
10230
+ */
10231
+ const staticDeps = context.staticMap.get(sourcePath);
10232
+ if (staticDeps) {
10233
+ for (const dep of staticDeps) {
10234
+ const depPathInfo = getPathInfo(dep);
10235
+ if (context.listPathSet.has(sourcePath) && depPathInfo.lastSegment === WILDCARD) {
10236
+ //expand indexes
10237
+ const newValue = context.stateProxy[getByAddressSymbol](address);
10238
+ const absPathInfo = getAbsolutePathInfo(context.stateElement, address.pathInfo);
10239
+ const absAddress = createAbsoluteStateAddress(absPathInfo, address.listIndex);
10240
+ const lastValue = getLastListValueByAbsoluteStateAddress(absAddress);
10241
+ const listDiff = createListDiff(address.listIndex, lastValue, newValue);
10242
+ const selection = selectExpansionIndexes(context, sourcePath, lastValue, newValue, listDiff);
10243
+ for (const listIndex of selection.fullRows) {
10244
+ const depAddress = createStateAddress(depPathInfo, listIndex);
10245
+ context.result.add(depAddress);
10246
+ nextEntries.push(depAddress);
10247
+ }
10248
+ if (selection.movedRows !== null) {
10249
+ const movedPathInfos = getMovedRowExpansionPaths(context, dep, depPathInfo);
10250
+ if (movedPathInfos === null) {
10251
+ // ネスト配下に index 依存 getter: 安全側で行全体を展開(従来挙動)
10252
+ for (const listIndex of selection.movedRows) {
10253
+ const depAddress = createStateAddress(depPathInfo, listIndex);
10254
+ context.result.add(depAddress);
10255
+ nextEntries.push(depAddress);
10256
+ }
10115
10257
  }
10116
- if (selection.movedRows !== null) {
10117
- const movedPathInfos = getMovedRowExpansionPaths(context, dep, depPathInfo);
10118
- if (movedPathInfos === null) {
10119
- // ネスト配下に index 依存 getter: 安全側で行全体を展開(従来挙動)
10120
- for (const listIndex of selection.movedRows) {
10121
- const depAddress = createStateAddress(depPathInfo, listIndex);
10258
+ else if (movedPathInfos.length > 0) {
10259
+ // 位置のみ変わった行は index 依存 getter のパスだけを展開する
10260
+ for (const listIndex of selection.movedRows) {
10261
+ for (const pathInfo of movedPathInfos) {
10262
+ const depAddress = createStateAddress(pathInfo, listIndex);
10122
10263
  context.result.add(depAddress);
10123
- nextEntries.push({ address: depAddress, depth: nextDepth });
10124
- }
10125
- }
10126
- else if (movedPathInfos.length > 0) {
10127
- // 位置のみ変わった行は index 依存 getter のパスだけを展開する
10128
- for (const listIndex of selection.movedRows) {
10129
- for (const pathInfo of movedPathInfos) {
10130
- const depAddress = createStateAddress(pathInfo, listIndex);
10131
- context.result.add(depAddress);
10132
- nextEntries.push({ address: depAddress, depth: nextDepth });
10133
- }
10264
+ nextEntries.push(depAddress);
10134
10265
  }
10135
10266
  }
10136
- // movedPathInfos が空: index を読む getter が subtree に無い =
10137
- // 位置のみ変わった行の値は不変。展開・dirty 化とも不要。
10138
10267
  }
10268
+ // movedPathInfos が空: index を読む getter が subtree に無い =
10269
+ // 位置のみ変わった行の値は不変。展開・dirty 化とも不要。
10139
10270
  }
10140
- else {
10141
- const depAddress = createStateAddress(depPathInfo, address.listIndex);
10142
- context.result.add(depAddress);
10143
- nextEntries.push({ address: depAddress, depth: nextDepth });
10144
- }
10271
+ }
10272
+ else {
10273
+ const depAddress = createStateAddress(depPathInfo, address.listIndex);
10274
+ context.result.add(depAddress);
10275
+ nextEntries.push(depAddress);
10145
10276
  }
10146
10277
  }
10147
- /**
10148
- * 動的依存関係をたどる
10149
- * 動的依存関係は、getterの実行時に決定される
10150
- *
10151
- * source, target
10152
- *
10153
- * products.*.price => products.*.tax
10154
- * get "products.*.tax"() { return this["products.*.price"] * 0.1; }
10155
- *
10156
- * products.*.price => products.summary
10157
- * get "products.summary"() { return this.$getAll("products.*.price", []).reduce(sum); }
10158
- *
10159
- * categories.*.name => categories.*.products.*.categoryName
10160
- * get "categories.*.products.*.categoryName"() { return this["categories.*.name"]; }
10161
- */
10162
- const dynamicDeps = context.dynamicMap.get(sourcePath);
10163
- if (dynamicDeps) {
10164
- for (const dep of dynamicDeps) {
10165
- const depPathInfo = getPathInfo(dep);
10166
- const listIndexes = [];
10167
- if (depPathInfo.wildcardCount > 0) {
10168
- // ワイルドカードを含む依存関係の処理
10169
- // 同じ親を持つかをパスの集合積で判定する
10170
- // polyfills.tsにてSetのintersectionメソッドを定義している
10171
- const wildcardLen = calcWildcardLen(address.pathInfo, depPathInfo);
10172
- const expandable = (depPathInfo.wildcardCount - wildcardLen) >= 1;
10173
- if (expandable) {
10174
- let listIndex;
10175
- if (wildcardLen > 0) {
10176
- // categories.*.name => categories.*.products.*.categoryName
10177
- // ワイルドカードを含む同じ親(products.*)を持つのが、
10178
- // さらに下位にワイルドカードがあるので展開する
10179
- if (address.listIndex === null) {
10180
- raiseError(`Cannot expand dynamic dependency with wildcard for non-list address: ${address.pathInfo.path}`);
10181
- }
10182
- listIndex = address.listIndex.at(wildcardLen - 1);
10183
- }
10184
- else {
10185
- // selectedIndex => items.*.selected
10186
- // 同じ親を持たない場合はnullから開始
10187
- listIndex = null;
10188
- }
10189
- const expandContext = {
10190
- stateElement: context.stateElement,
10191
- targetListIndexes: [],
10192
- wildcardPaths: depPathInfo.wildcardPaths,
10193
- wildcardParentPaths: depPathInfo.wildcardParentPaths,
10194
- stateProxy: context.stateProxy,
10195
- searchType: context.searchType,
10196
- };
10197
- _walkExpandWildcard(expandContext, wildcardLen, listIndex);
10198
- listIndexes.push(...expandContext.targetListIndexes);
10199
- }
10200
- else {
10201
- // products.*.price => products.*.tax
10202
- // ワイルドカードを含む同じ親(products.*)を持つので、リストインデックスは引き継ぐ
10278
+ }
10279
+ /**
10280
+ * 動的依存関係をたどる
10281
+ * 動的依存関係は、getterの実行時に決定される
10282
+ *
10283
+ * source, target
10284
+ *
10285
+ * products.*.price => products.*.tax
10286
+ * get "products.*.tax"() { return this["products.*.price"] * 0.1; }
10287
+ *
10288
+ * products.*.price => products.summary
10289
+ * get "products.summary"() { return this.$getAll("products.*.price", []).reduce(sum); }
10290
+ *
10291
+ * categories.*.name => categories.*.products.*.categoryName
10292
+ * get "categories.*.products.*.categoryName"() { return this["categories.*.name"]; }
10293
+ */
10294
+ const dynamicDeps = context.dynamicMap.get(sourcePath);
10295
+ if (dynamicDeps) {
10296
+ for (const dep of dynamicDeps) {
10297
+ const depPathInfo = getPathInfo(dep);
10298
+ const listIndexes = [];
10299
+ if (depPathInfo.wildcardCount > 0) {
10300
+ // ワイルドカードを含む依存関係の処理
10301
+ // 同じ親を持つかをパスの集合積で判定する
10302
+ // polyfills.tsにてSetのintersectionメソッドを定義している
10303
+ const wildcardLen = calcWildcardLen(address.pathInfo, depPathInfo);
10304
+ const expandable = (depPathInfo.wildcardCount - wildcardLen) >= 1;
10305
+ if (expandable) {
10306
+ let listIndex;
10307
+ if (wildcardLen > 0) {
10308
+ // categories.*.name => categories.*.products.*.categoryName
10309
+ // ワイルドカードを含む同じ親(products.*)を持つのが、
10310
+ // さらに下位にワイルドカードがあるので展開する
10203
10311
  if (address.listIndex === null) {
10204
10312
  raiseError(`Cannot expand dynamic dependency with wildcard for non-list address: ${address.pathInfo.path}`);
10205
10313
  }
10206
- const listIndex = address.listIndex.at(wildcardLen - 1);
10207
- listIndexes.push(listIndex);
10314
+ listIndex = address.listIndex.at(wildcardLen - 1);
10208
10315
  }
10316
+ else {
10317
+ // selectedIndex => items.*.selected
10318
+ // 同じ親を持たない場合はnullから開始
10319
+ listIndex = null;
10320
+ }
10321
+ const expandContext = {
10322
+ stateName: context.stateName,
10323
+ stateElement: context.stateElement,
10324
+ targetListIndexes: [],
10325
+ wildcardPaths: depPathInfo.wildcardPaths,
10326
+ wildcardParentPaths: depPathInfo.wildcardParentPaths,
10327
+ stateProxy: context.stateProxy,
10328
+ searchType: context.searchType,
10329
+ };
10330
+ _walkExpandWildcard(expandContext, wildcardLen, listIndex);
10331
+ listIndexes.push(...expandContext.targetListIndexes);
10209
10332
  }
10210
10333
  else {
10211
- // products.*.tax => currentTaxRate
10212
- // 同じ親を持たないので、リストインデックスはnull
10213
- listIndexes.push(null);
10214
- }
10215
- for (const listIndex of listIndexes) {
10216
- const depAddress = createStateAddress(depPathInfo, listIndex);
10217
- context.result.add(depAddress);
10218
- nextEntries.push({ address: depAddress, depth: nextDepth });
10334
+ // products.*.price => products.*.tax
10335
+ // ワイルドカードを含む同じ親(products.*)を持つので、リストインデックスは引き継ぐ
10336
+ if (address.listIndex === null) {
10337
+ raiseError(`Cannot expand dynamic dependency with wildcard for non-list address: ${address.pathInfo.path}`);
10338
+ }
10339
+ const listIndex = address.listIndex.at(wildcardLen - 1);
10340
+ listIndexes.push(listIndex);
10219
10341
  }
10220
10342
  }
10221
- }
10222
- // 逆順でpushして、元の再帰と同じ探索順序を保つ
10223
- for (let i = nextEntries.length - 1; i >= 0; i--) {
10224
- stack.push(nextEntries[i]);
10343
+ else {
10344
+ // products.*.tax => currentTaxRate
10345
+ // 同じ親を持たないので、リストインデックスはnull
10346
+ listIndexes.push(null);
10347
+ }
10348
+ for (const listIndex of listIndexes) {
10349
+ const depAddress = createStateAddress(depPathInfo, listIndex);
10350
+ context.result.add(depAddress);
10351
+ nextEntries.push(depAddress);
10352
+ }
10225
10353
  }
10226
10354
  }
10227
10355
  }
@@ -10235,7 +10363,12 @@ function walkDependency(stateName, stateElement, startAddress, staticDependency,
10235
10363
  callback(startAddress);
10236
10364
  return [];
10237
10365
  }
10366
+ // パス単位のトポロジカル順位。値を一切読まずに求まり、依存グラフは追記のみで
10367
+ // 成長するため epoch でメモ化される(topologicalRank.ts)。
10368
+ const ranks = getTopologicalRanks(startPath, staticDependency, dynamicDependency, MAX_DEPENDENCY_DEPTH);
10238
10369
  const context = {
10370
+ ranks: ranks,
10371
+ stateName: stateName,
10239
10372
  stateElement: stateElement,
10240
10373
  staticMap: staticDependency,
10241
10374
  dynamicMap: dynamicDependency,