@aiquants/virtualscroll 3.10.0 → 3.11.1

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/src/logger.ts CHANGED
@@ -47,7 +47,7 @@ export interface ILogger {
47
47
  */
48
48
  export class Logger implements ILogger {
49
49
  private level: LogLevel
50
- private prefix: string
50
+ private readonly prefix: string
51
51
  private impl: ILogger
52
52
 
53
53
  /**
@@ -104,26 +104,6 @@ export class Logger implements ILogger {
104
104
  this.impl = impl
105
105
  }
106
106
 
107
- /**
108
- * @method setPrefix
109
- * @description Updates the log prefix for the static instance.
110
- * @description 静的インスタンスのログのプレフィックスを更新します。
111
- * @param {string} prefix - The new prefix.
112
- */
113
- static setPrefix(prefix: string): void {
114
- Logger.instance.setPrefix(prefix)
115
- }
116
-
117
- /**
118
- * @method setPrefix
119
- * @description Updates the log prefix.
120
- * @description ログのプレフィックスを更新します。
121
- * @param {string} prefix - The new prefix.
122
- */
123
- setPrefix(prefix: string): void {
124
- this.prefix = prefix
125
- }
126
-
127
107
  /**
128
108
  * @method isEnabled
129
109
  * @description Returns whether the given level would be emitted at the current level (static instance).
@@ -197,10 +177,6 @@ export class Logger implements ILogger {
197
177
  }
198
178
  }
199
179
 
200
- static info(message?: unknown, ...optionalParams: unknown[]): void {
201
- Logger.instance.info(message, ...optionalParams)
202
- }
203
-
204
180
  info(message?: unknown, ...optionalParams: unknown[]): void {
205
181
  if (this.level <= LogLevel.INFO) {
206
182
  this.impl.info(...this.formatMessage(Logger.resolveLazy(message)), ...optionalParams.map(Logger.resolveLazy))
@@ -11,7 +11,7 @@
11
11
  * それを管理するための React フック `useFenwickMapTree` の提供。
12
12
  * 動的なアイテムサイズを持つ仮想スクロールのシナリオに最適化されている。
13
13
  */
14
- import { useRef } from "react"
14
+ import { useCallback, useReducer, useRef } from "react"
15
15
  import { Logger } from "./logger.ts"
16
16
  import { minmax } from "./utils.ts"
17
17
 
@@ -33,20 +33,15 @@ type DeltaUpdate = { index: number; change: number }
33
33
  type ValueUpdate = { index: number; value: number }
34
34
 
35
35
  /**
36
- * Converts a numeric size into a non-negative bigint, safeguarding against fractional input.
36
+ * The options of a tree read that only looks rows up and never materialises them (`prefixSum`, `get`, `getTotal`,
37
+ * `findIndexAtOrAfter`, `findIndexAtOrBefore`): the read leaves the stored values, the size and `revision` as they are. One
38
+ * frozen value that every lookup passes, so no caller spells the option out. Internal to the package (not in the barrel).
37
39
  *
38
- * 分数入力を安全に丸めてから非負の bigint へ変換。
40
+ * 行を引くだけで具現化しない木の読み取り (`prefixSum`・`get`・`getTotal`・`findIndexAtOrAfter`・`findIndexAtOrBefore`) の選択肢。
41
+ * 読み取りは保持する値・要素数・`revision` をそのまま残す。どの引き当ても渡す凍結した 1 つの値で、呼び出し側は選択肢を書き下さない。
42
+ * パッケージの内部用 (バレル非公開)。
39
43
  */
40
- const toSafeBigInt = (value: number): bigint => {
41
- if (!Number.isFinite(value)) {
42
- return 0n
43
- }
44
- const truncated = Math.trunc(value)
45
- if (truncated <= 0) {
46
- return 0n
47
- }
48
- return BigInt(truncated)
49
- }
44
+ export const FENWICK_LOOKUP_ONLY = Object.freeze({ materializeOption: Object.freeze({ materialize: false }) })
50
45
 
51
46
  /**
52
47
  * Validates that `valueFn` returned a finite number, throwing otherwise. A single NaN/Infinity
@@ -75,14 +70,17 @@ const requireFiniteValue = (value: number, index: number): number => {
75
70
  const NEGATIVE_EFFECTIVE_VALUE_EPSILON = 1e-9
76
71
 
77
72
  /**
78
- * Derives the lowest set bit without relying on 32-bit bitwise operators.
73
+ * Derives the lowest set bit of a 1-based tree index (a positive integer) without relying on 32-bit bitwise operators. Every
74
+ * caller walks the tree from such an index, so the result is at least 1 and each walk ends: a walk up adds it until the index
75
+ * passes the size, a walk down subtracts it until the index reaches 0.
76
+ *
77
+ * 1 始まりの木の添字 (正の整数) の最下位ビットを、32 ビット演算に依存せずに求める処理。呼び出し元はどれもそのような添字から木を
78
+ * 辿るので、結果は 1 以上で、どの辿り方も終わる (上へ辿ると足して添字が要素数を越え、下へ辿ると引いて添字が 0 に着く)。
79
79
  *
80
- * 32 ビット演算に依存せずに最下位ビットを算出。
80
+ * @param value - A 1-based tree index (a positive integer) / 1 始まりの木の添字 (正の整数)
81
+ * @returns The lowest set bit (1 or more) / 最下位ビット (1 以上)
81
82
  */
82
83
  const getLowestSetBit = (value: number): number => {
83
- if (value <= 0 || !Number.isFinite(value)) {
84
- return 0
85
- }
86
84
  const integer = Math.trunc(value)
87
85
  // 32 ビット符号付き整数の範囲内なら通常のビット演算で最下位ビットを求める (BigInt 割り当てを回避)。
88
86
  // ホットパス (prefixSum / _updateTree など) の GC 圧を大幅に削減する。
@@ -154,6 +152,13 @@ export class FenwickMapTree {
154
152
  private valueFn?: (index: number) => number
155
153
  private total?: number
156
154
 
155
+ /**
156
+ * @private
157
+ * @property {number} revisionCount - The counter behind `revision`: raised by every change of a stored value or of the size.
158
+ * @property {number} revisionCount - `revision` の元の数。保持する値か要素数が変わるたびに上がる。
159
+ */
160
+ private revisionCount = 0
161
+
157
162
  /**
158
163
  * @constructor
159
164
  * @description Initializes the Fenwick Tree.
@@ -193,6 +198,7 @@ export class FenwickMapTree {
193
198
  this.valueFn = undefined
194
199
  this.baseValue = valueOrFn
195
200
  this.total = this.baseValue * this.size
201
+ this.revisionCount += 1
196
202
  return
197
203
  }
198
204
 
@@ -276,11 +282,9 @@ export class FenwickMapTree {
276
282
  this.valueFn = valueOrFn
277
283
  this.baseValue = baseValue
278
284
  if (seed) {
285
+ // 種は [from, to] (to < size) の値なので、添字は要素数を越えない
279
286
  for (let i = 0; i < seed.values.length; i++) {
280
287
  const index = seed.from + i
281
- if (index >= this.size) {
282
- break
283
- }
284
288
  // _materialize を呼ぶ代わりに、計算フェーズの検証済みの値を使って更新する (二重評価を回避)
285
289
  const change = seed.values[i] - this.baseValue
286
290
  // baseValue と一致する行は delta を持たせない (Map の無駄な肥大を回避)
@@ -292,6 +296,7 @@ export class FenwickMapTree {
292
296
  }
293
297
  // 具現化が完了した後に total を計算する
294
298
  this.total = this.getTotal()
299
+ this.revisionCount += 1
295
300
  }
296
301
 
297
302
  /**
@@ -318,6 +323,7 @@ export class FenwickMapTree {
318
323
  // baseValue を差し替えたら total を再計算しないと prefixSum と乖離する。
319
324
  // (deltas は温存されるため sum(deltas) + baseValue * size に一致させる)
320
325
  this.total = this._computeTreeTotal()
326
+ this.revisionCount += 1
321
327
  }
322
328
  }
323
329
 
@@ -348,10 +354,6 @@ export class FenwickMapTree {
348
354
  * @returns {number} The representative base value.
349
355
  */
350
356
  private static _modeOrMedian(values: number[]): number {
351
- if (values.length === 0) {
352
- return 0
353
- }
354
-
355
357
  // 中央値を計算してデフォルトの最頻値として設定
356
358
  values.sort((a, b) => a - b)
357
359
  const mid = Math.floor(values.length / 2)
@@ -402,10 +404,6 @@ export class FenwickMapTree {
402
404
  * @returns {number} The estimated base value.
403
405
  */
404
406
  private static _sampleBaseValueStrided(valueFn: (index: number) => number, size: number, sampleCount: number): number {
405
- if (size <= 0) {
406
- return 0
407
- }
408
-
409
407
  // stride = max(1, ceil(size / sampleCount)) を基準に、偶数なら +1 して奇数化する。
410
408
  // ceil によりサンプル数を多少犠牲にしても必ずリスト全域を跨ぐ (_modeOrMedian の
411
409
  // 20% しきい値は割合ベースのためサンプル数減の影響は受けない)。
@@ -526,15 +524,12 @@ export class FenwickMapTree {
526
524
  if (change === 0) {
527
525
  return
528
526
  }
527
+ this.revisionCount += 1
529
528
  // tree Map を更新
530
529
  let treeIndex = index + 1 // Fenwick Tree のアルゴリズムは 1 始まりで設計される
531
530
  while (treeIndex <= this.size) {
532
531
  this.tree.set(treeIndex, (this.tree.get(treeIndex) ?? 0) + change)
533
- const step = getLowestSetBit(treeIndex)
534
- if (step === 0) {
535
- break
536
- }
537
- treeIndex += step
532
+ treeIndex += getLowestSetBit(treeIndex)
538
533
  }
539
534
 
540
535
  // 合計値を更新 (totalが計算済みの場合のみ)
@@ -575,7 +570,8 @@ export class FenwickMapTree {
575
570
  }
576
571
 
577
572
  // 現在の delta はバッチ内で既に更新済みならその値を、なければ Map の値を使う
578
- const currentDelta = pendingDeltas.has(index) ? (pendingDeltas.get(index) ?? 0) : (this.deltas.get(index) ?? 0)
573
+ const pendingDelta = pendingDeltas.get(index)
574
+ const currentDelta = pendingDelta === undefined ? (this.deltas.get(index) ?? 0) : pendingDelta
579
575
  const oldValue = currentDelta + this.baseValue
580
576
  const change = value - oldValue
581
577
  if (change !== 0) {
@@ -603,11 +599,7 @@ export class FenwickMapTree {
603
599
  let treeIndex = this.size
604
600
  while (treeIndex > 0) {
605
601
  sum += this.tree.get(treeIndex) ?? 0
606
- const step = getLowestSetBit(treeIndex)
607
- if (step === 0) {
608
- break
609
- }
610
- treeIndex -= step
602
+ treeIndex -= getLowestSetBit(treeIndex)
611
603
  }
612
604
 
613
605
  return sum + this.baseValue * this.size
@@ -717,7 +709,7 @@ export class FenwickMapTree {
717
709
  return this._findIndexLarge(target, options, chooseLowerBound)
718
710
  }
719
711
  if (this.size === 0) {
720
- return { index: -1, total: this.total ?? 0, cumulative: undefined, currentValue: undefined, safeIndex: undefined }
712
+ return { index: -1, total: this.getTotal(), cumulative: undefined, currentValue: undefined, safeIndex: undefined }
721
713
  }
722
714
 
723
715
  // 探索前に対象範囲を具現化しておく。具現化後のツリー状態で降下を行うことで、
@@ -737,7 +729,7 @@ export class FenwickMapTree {
737
729
  resultIdx = this._descend(target, chooseLowerBound)
738
730
 
739
731
  if (resultIdx < 0 || resultIdx >= this.size) {
740
- return { index: -1, total: this.total ?? this.getTotal(), cumulative: undefined, currentValue: undefined, safeIndex: undefined }
732
+ return { index: -1, total: this.getTotal(), cumulative: undefined, currentValue: undefined, safeIndex: undefined }
741
733
  }
742
734
 
743
735
  if (!materialize) {
@@ -759,7 +751,7 @@ export class FenwickMapTree {
759
751
 
760
752
  return {
761
753
  index: resultIdx,
762
- total: this.total ?? result.total,
754
+ total: this.getTotal(),
763
755
  cumulative: result.cumulative,
764
756
  currentValue: result.currentValue,
765
757
  safeIndex: result.safeIndex,
@@ -812,14 +804,8 @@ export class FenwickMapTree {
812
804
  * 非常に大きなサイズに対して bigint 演算を用いた二分探索を実施。
813
805
  */
814
806
  private _findIndexLarge(target: number, options: MaterializeConfig, chooseLowerBound: boolean): { index: number; total: number | undefined; cumulative: number | undefined; currentValue: number | undefined; safeIndex: number | undefined } {
815
- if (this.size === 0) {
816
- return { index: -1, total: this.total ?? 0, cumulative: undefined, currentValue: undefined, safeIndex: undefined }
817
- }
818
-
819
- const sizeBig = toSafeBigInt(this.size)
820
- if (sizeBig === 0n) {
821
- return { index: -1, total: this.total ?? 0, cumulative: undefined, currentValue: undefined, safeIndex: undefined }
822
- }
807
+ // 呼び出し元 (_findIndex) は要素数 2^30 以上のときだけここへ来るので、要素数は正の整数
808
+ const sizeBig = BigInt(this.size)
823
809
 
824
810
  // 小サイズパス (_findIndex) と同一契約: 探索前に対象範囲を具現化し、探索中は木を
825
811
  // 変異させない (prefixSum に options を渡さない)。materialize:true の場合は
@@ -904,7 +890,7 @@ export class FenwickMapTree {
904
890
  // 空の木 (size=0) は正当な状態として中立値を返す (throw しない)。
905
891
  // minmax(index, 0, -1) が負値へ丸められ get(-1) が throw するのを回避する。
906
892
  if (this.size === 0) {
907
- return { cumulative: 0, total: this.total ?? 0, currentValue: 0, safeIndex: 0 }
893
+ return { cumulative: 0, total: this.getTotal(), currentValue: 0, safeIndex: 0 }
908
894
  }
909
895
  // 小数 index は具現化時に非整数キーのノードを作り整数走査と乖離するため切り捨てる
910
896
  // (Infinity は trunc を素通りした後 minmax で size-1 にクランプされる)。
@@ -918,11 +904,7 @@ export class FenwickMapTree {
918
904
  while (treeIndex > 0) {
919
905
  const treeNodeValue = this.tree.get(treeIndex) ?? 0
920
906
  sum += treeNodeValue
921
- const step = getLowestSetBit(treeIndex)
922
- if (step === 0) {
923
- break
924
- }
925
- treeIndex -= step
907
+ treeIndex -= getLowestSetBit(treeIndex)
926
908
  }
927
909
 
928
910
  const currentValue = materializeOption?.materialize ? this.get(safeIndex) : (this.deltas.get(safeIndex) || 0) + this.baseValue
@@ -972,10 +954,6 @@ export class FenwickMapTree {
972
954
  this.total = 0
973
955
  } else {
974
956
  this.total = this._computeTreeTotal()
975
- const lastPrefix = this.prefixSum(this.getSize() - 1)
976
- if (lastPrefix.cumulative !== lastPrefix.total) {
977
- Logger.error("Inconsistent Fenwick Tree state")
978
- }
979
957
  }
980
958
  }
981
959
 
@@ -987,11 +965,11 @@ export class FenwickMapTree {
987
965
  * @description Rebuilds the Fenwick Tree from the existing `baseValue` and `deltas`. This corrects any discrepancies in the tree's internal state, such as those caused by floating-point errors, by recalculating the tree structure and the total sum from the source `deltas`. This method does not re-materialize values from `valueFn`.
988
966
  * @description 既存の `baseValue` と `deltas` から Fenwick Tree を再構築します。これにより、`deltas` からツリー構造と合計値を再計算することで、浮動小数点誤差などによって生じた内部状態の不一致を修正します。このメソッドは `valueFn` から値を再具現化しません。
989
967
  * @param {object} [options] - Optional settings for rebuilding.
990
- * @param {boolean} [options.materialize=false] - If true and `valueFn` is provided, re-materializes all values, recalculating `deltas` and `baseValue`.
968
+ * @param {boolean} [options.materialize=false] - If true and `valueFn` is set, rebuilds from `valueFn` instead: a reset with materialization (`reset(size, valueFn, { materialize: true })`) that re-samples `baseValue`, materializes the initial window (rows 0 to `SAMPLE_COUNT - 1`) and discards every other measured delta. A tree with a uniform value (no `valueFn`) rebuilds from its deltas either way. / true かつ `valueFn` があれば、代わりに `valueFn` から作り直す。具現化付きのリセット (`reset(size, valueFn, { materialize: true })`) で、`baseValue` を標本で決め直し、初期の窓 (行 0〜`SAMPLE_COUNT - 1`) を具現化し、ほかの測った差分はすべて捨てる。一様な値の木 (`valueFn` 無し) はどちらでも差分から作り直す。
991
969
  */
992
970
  rebuildTree(options?: { materialize?: boolean }) {
993
971
  if (options?.materialize && this.valueFn) {
994
- // すべての値を具現化する
972
+ // 全行の具現化は巨大な木で O(n) の valueFn 呼び出しになるので、リセットと同じく初期の窓だけを具現化する
995
973
  const valueFn = this.valueFn
996
974
  this.reset(this.size, (i) => valueFn(i), { materialize: true })
997
975
  return
@@ -1001,25 +979,21 @@ export class FenwickMapTree {
1001
979
  // let newTotal = this.baseValue * this.size
1002
980
 
1003
981
  // 既存の deltas を使って新しいツリーを構築し、合計値も同時に再計算する
982
+ // 0 の delta は保持しない (zero-delta 枝刈り) ので、どの delta も木へ伝える
1004
983
  for (const [index, delta] of this.deltas.entries()) {
1005
- if (delta === 0) {
1006
- continue
1007
- }
1008
984
  // Fenwick Tree のアルゴリズムは 1 始まりで設計されるため、インデックスを 1 加算する
1009
985
  let treeIndex = index + 1
1010
986
  while (treeIndex <= this.size) {
1011
987
  newTree.set(treeIndex, (newTree.get(treeIndex) ?? 0) + delta)
1012
- const step = getLowestSetBit(treeIndex)
1013
- if (step === 0) {
1014
- break
1015
- }
1016
- treeIndex += step
988
+ treeIndex += getLowestSetBit(treeIndex)
1017
989
  }
1018
990
  }
1019
991
 
1020
992
  // 最後に状態をアトミックに更新
1021
993
  this.tree = newTree
1022
994
  this.total = this._computeTreeTotal()
995
+ // 値は変えないが、浮動小数点の誤差を直した接頭辞和は変わり得る
996
+ this.revisionCount += 1
1023
997
  }
1024
998
 
1025
999
  /**
@@ -1034,11 +1008,6 @@ export class FenwickMapTree {
1034
1008
  * @returns {number} The difference between the cached total and the theoretical total.
1035
1009
  */
1036
1010
  calculateAccumulatedError(): number {
1037
- if (this.total === undefined) {
1038
- // total がまだ計算されていない場合は、誤差は 0 とする
1039
- return 0
1040
- }
1041
-
1042
1011
  // 理論上の合計値を計算する。範囲外へ退避している delta は木に載っていないので数えない
1043
1012
  let theoreticalTotal = this.baseValue * this.size
1044
1013
  for (const [index, delta] of this.deltas) {
@@ -1048,7 +1017,7 @@ export class FenwickMapTree {
1048
1017
  }
1049
1018
 
1050
1019
  // キャッシュされている合計値との差を返す
1051
- return this.total - theoreticalTotal
1020
+ return this.getTotal() - theoreticalTotal
1052
1021
  }
1053
1022
 
1054
1023
  /**
@@ -1140,11 +1109,7 @@ export class FenwickMapTree {
1140
1109
  this.size = targetSize
1141
1110
  this.rebuildTree()
1142
1111
  }
1143
-
1144
- const lastPrefix = this.prefixSum(this.getSize() - 1)
1145
- if (lastPrefix.cumulative !== lastPrefix.total) {
1146
- Logger.error("Inconsistent Fenwick Tree state")
1147
- }
1112
+ this.revisionCount += 1
1148
1113
  }
1149
1114
 
1150
1115
  /**
@@ -1190,20 +1155,12 @@ export class FenwickMapTree {
1190
1155
  // 既存範囲 (<= oldSize) のノードは更新済みなので、
1191
1156
  // 親チェーンを辿って oldSize を超える最初のノードまで進める。
1192
1157
  while (treeIndex <= oldSize) {
1193
- const step = getLowestSetBit(treeIndex)
1194
- if (step === 0) {
1195
- break
1196
- }
1197
- treeIndex += step
1158
+ treeIndex += getLowestSetBit(treeIndex)
1198
1159
  }
1199
1160
  // 新たに有効化されたノード (oldSize, newSize] にのみ差分を伝播する。
1200
1161
  while (treeIndex <= newSize) {
1201
1162
  this.tree.set(treeIndex, (this.tree.get(treeIndex) ?? 0) + delta)
1202
- const step = getLowestSetBit(treeIndex)
1203
- if (step === 0) {
1204
- break
1205
- }
1206
- treeIndex += step
1163
+ treeIndex += getLowestSetBit(treeIndex)
1207
1164
  }
1208
1165
  }
1209
1166
 
@@ -1246,9 +1203,8 @@ export class FenwickMapTree {
1246
1203
 
1247
1204
  // total は増分 (+= baseValue * Δ) ではなく prefixSum と同一走査を再現する
1248
1205
  // _computeTreeTotal で確定する。増分加算は浮動小数点の分配則不成立により
1249
- // prefixSum の走査結果と厳密不一致になり、changeSize 末尾の整合検査が
1250
- // 偽の "Inconsistent Fenwick Tree state" を量産するため。O(log n) なので
1251
- // 増分更新の速度メリットは保たれる。
1206
+ // prefixSum(size - 1) の走査結果と厳密には一致せず、末尾の位置で total と
1207
+ // 最後の行の下端が食い違うため。O(log n) なので増分更新の速度メリットは保たれる。
1252
1208
  this.total = this._computeTreeTotal()
1253
1209
  }
1254
1210
 
@@ -1291,6 +1247,23 @@ export class FenwickMapTree {
1291
1247
  return this.baseValue
1292
1248
  }
1293
1249
 
1250
+ /**
1251
+ * @description Gets the tree's revision: a counter that rises whenever a value the tree stores or its size changes — an
1252
+ * update, a materialization that changes a row, a reset, a resize, a new uniform value or a rebuild — and stays the same
1253
+ * otherwise (reads, a materialization that finds the stored value, an update to the same value, a resize to the same size).
1254
+ * The tree keeps its identity while its contents change, so a caller that derives anything from the prefix sums (row tops,
1255
+ * a rendering range) compares two reads: equal reads mean nothing it derived became stale in between. The counter only
1256
+ * rises; how far it rises per change is unspecified.
1257
+ * @description 木の版を取得。木が保持する値か要素数が変わるたび (更新・行を変える具現化・リセット・要素数の変更・一様な値の
1258
+ * 差し替え・再構築) に上がり、それ以外 (読み取り・保持する値と同じ値を見つけた具現化・同じ値への更新・同じ要素数への変更) では
1259
+ * 変わらない数。木は中身が変わっても同一性を保つため、接頭辞和から何か (行の上端・描画範囲) を導く呼び出し側は 2 回の読み取りを
1260
+ * 比べ、等しければその間に導いたものは古くなっていない。数は上がるだけで、1 回の変化でいくつ上がるかは決めない。
1261
+ * @returns {number} The revision. 版。
1262
+ */
1263
+ get revision(): number {
1264
+ return this.revisionCount
1265
+ }
1266
+
1294
1267
  /**
1295
1268
  * @method findIndexAtOrAfter
1296
1269
  * @description Finds the first index where the cumulative sum is greater than or equal to a target value.
@@ -1470,3 +1443,73 @@ export const useFenwickMapTree = (size: number, valueOrFn: number | ((index: num
1470
1443
 
1471
1444
  return tree
1472
1445
  }
1446
+
1447
+ /**
1448
+ * What `useFenwickTreeRevision` returns: the revision the memos that read one tree depend on, and the one way to change that
1449
+ * tree outside render so that they see the change.
1450
+ *
1451
+ * `useFenwickTreeRevision` の戻り値。1 本の木を読む memo が依存する版数と、memo が変化に気付くように描画の外でその木を変える
1452
+ * ただ 1 つの方法。
1453
+ */
1454
+ export type FenwickTreeRevision = {
1455
+ /** Advances by one per change made through `change` that moved the tree's revision / `change` を通した変更が木の版を動かすたびに 1 つ進む数 */
1456
+ readonly revision: number
1457
+ /** Runs one change of the tree made outside render and returns its result; advances `revision` only when the tree's revision moved / 描画の外で行う木の変更を 1 回実行してその結果を返し、木の版が動いたときだけ `revision` を進める処理 */
1458
+ readonly change: <Result>(mutate: () => Result) => Result
1459
+ }
1460
+
1461
+ /**
1462
+ * Advances a component's tree revision by one (the reducer behind `FenwickTreeRevision["revision"]`).
1463
+ *
1464
+ * コンポーネントの木の版数を 1 つ進める処理 (`FenwickTreeRevision["revision"]` の reducer)。
1465
+ *
1466
+ * @param revision - The current revision / 今の版数
1467
+ * @returns The next revision / 次の版数
1468
+ */
1469
+ const nextTreeRevision = (revision: number): number => revision + 1
1470
+
1471
+ /**
1472
+ * Ties the memos of a component to the contents of a Fenwick tree. The tree keeps its identity while its contents change
1473
+ * (`useFenwickMapTree`), so a memo that derives anything from its prefix sums (row tops, a rendering range, the placed
1474
+ * columns) cannot see a change through the tree itself, and the total is no signal either: a batch of changes that cancel out
1475
+ * leaves it as it was. Every change made outside render goes through `change`, which advances `revision` exactly when the
1476
+ * tree's own revision (`FenwickMapTree.revision`) moved, so the next commit recomputes the memos that depend on `revision`,
1477
+ * and a change that stores the same values (an update to the value a row already has, a materialisation that finds the stored
1478
+ * value) adds no commit. Changes made during render (a reset or a resize by `useFenwickMapTree`) come with new props, which the
1479
+ * memos depend on already. Internal to the package (not in the barrel).
1480
+ *
1481
+ * コンポーネントの memo を Fenwick 木の中身に結び付けるフック。木は中身が変わっても同一性を保つ (`useFenwickMapTree`) ので、
1482
+ * 接頭辞和から何か (行の上端・描画範囲・配置した列) を導く memo は木そのものからは変化に気付けず、総和も合図にならない (打ち消し合う
1483
+ * 変更の組は総和を変えない)。描画の外の変更はすべて `change` を通し、`change` は木自身の版 (`FenwickMapTree.revision`) が動いたとき
1484
+ * ちょうどに `revision` を進める。だから次の確定は `revision` に依存する memo を計算し直し、同じ値を保つ変更 (行が既に持つ値への更新・
1485
+ * 保持する値と同じ値を見つけた具現化) は確定を足さない。描画中の変更 (`useFenwickMapTree` のリセットと大きさの変更) は新しい props と
1486
+ * 一緒に来るので、memo は既にそれに依存している。パッケージの内部用 (バレル非公開)。
1487
+ *
1488
+ * @param tree - The tree whose changes the memos must see / memo が変化に気付くべき木
1489
+ * @returns The revision and the change runner / 版数と変更の実行関数
1490
+ */
1491
+ export const useFenwickTreeRevision = (tree: FenwickMapTree): FenwickTreeRevision => {
1492
+ const [revision, advanceRevision] = useReducer(nextTreeRevision, 0)
1493
+
1494
+ /**
1495
+ * Runs one change of the tree and advances the revision when the tree's revision moved.
1496
+ *
1497
+ * 木の変更を 1 回実行し、木の版が動いたら版数を進める処理。
1498
+ *
1499
+ * @param mutate - The change of the tree / 木の変更
1500
+ * @returns What `mutate` returned / `mutate` の戻り値
1501
+ */
1502
+ const change = useCallback(
1503
+ <Result>(mutate: () => Result): Result => {
1504
+ const revisionBefore = tree.revision
1505
+ const result = mutate()
1506
+ if (tree.revision !== revisionBefore) {
1507
+ advanceRevision()
1508
+ }
1509
+ return result
1510
+ },
1511
+ [tree],
1512
+ )
1513
+
1514
+ return { revision, change }
1515
+ }
@@ -90,6 +90,16 @@ class DoublyLinkedList<K, V> {
90
90
  node.next = null
91
91
  }
92
92
 
93
+ /**
94
+ * Returns the head of the list (the least recently used item) without removing it.
95
+ * リストの先頭 (最も最近使用されていないアイテム) を外さずに返す処理。
96
+ *
97
+ * @returns {DoublyLinkedListNode<K, V> | null} The head node, or null if the list is empty / 先頭のノード (空なら null)
98
+ */
99
+ peekHead(): DoublyLinkedListNode<K, V> | null {
100
+ return this.head
101
+ }
102
+
93
103
  /**
94
104
  * @method removeHead
95
105
  * @description Removes and returns the head of the list, which is the least recently used item.
@@ -176,14 +186,10 @@ export function useLruCache<K, V>(capacity: number) {
176
186
  if (!Number.isFinite(normalizedCapacity)) {
177
187
  return
178
188
  }
179
- while (cache.current.size > normalizedCapacity) {
180
- const lruNode = list.current.removeHead()
181
- if (lruNode) {
182
- cache.current.delete(lruNode.key)
183
- } else {
184
- // This should not happen if cache.current.size > 0
185
- break
186
- }
189
+ // 使用順の一覧と Map は同じ項目を持つので、Map が容量を超えている間は一覧の先頭が必ずある
190
+ for (let lruNode = list.current.peekHead(); lruNode !== null && cache.current.size > normalizedCapacity; lruNode = list.current.peekHead()) {
191
+ list.current.remove(lruNode)
192
+ cache.current.delete(lruNode.key)
187
193
  }
188
194
  }, [capacity, normalizedCapacity])
189
195