@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/CHANGELOG.md +102 -0
- package/README.md +44 -12
- package/dist/ScrollBar.d.cts +7 -6
- package/dist/ScrollBar.d.ts +7 -6
- package/dist/ScrollBar.d.ts.map +1 -1
- package/dist/ScrollPane.d.ts.map +1 -1
- package/dist/VirtualGrid.d.cts +18 -10
- package/dist/VirtualGrid.d.ts +18 -10
- package/dist/VirtualGrid.d.ts.map +1 -1
- package/dist/VirtualScroll.d.cts +119 -14
- package/dist/VirtualScroll.d.ts +119 -14
- package/dist/VirtualScroll.d.ts.map +1 -1
- package/dist/cli.js +26 -47
- package/dist/index.cjs +1 -1
- package/dist/index.js +2149 -2194
- package/dist/logger.d.cts +1 -16
- package/dist/logger.d.ts +1 -16
- package/dist/logger.d.ts.map +1 -1
- package/dist/useFenwickMapTree.d.cts +69 -1
- package/dist/useFenwickMapTree.d.ts +69 -1
- package/dist/useFenwickMapTree.d.ts.map +1 -1
- package/dist/useLruCache.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ScrollBar.tsx +60 -10
- package/src/ScrollPane.tsx +58 -15
- package/src/VirtualGrid.tsx +139 -110
- package/src/VirtualScroll.tsx +370 -215
- package/src/logger.ts +1 -25
- package/src/useFenwickMapTree.ts +138 -95
- package/src/useLruCache.ts +14 -8
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))
|
package/src/useFenwickMapTree.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
40
|
+
* 行を引くだけで具現化しない木の読み取り (`prefixSum`・`get`・`getTotal`・`findIndexAtOrAfter`・`findIndexAtOrBefore`) の選択肢。
|
|
41
|
+
* 読み取りは保持する値・要素数・`revision` をそのまま残す。どの引き当ても渡す凍結した 1 つの値で、呼び出し側は選択肢を書き下さない。
|
|
42
|
+
* パッケージの内部用 (バレル非公開)。
|
|
39
43
|
*/
|
|
40
|
-
const
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
816
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1250
|
-
//
|
|
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
|
+
}
|
package/src/useLruCache.ts
CHANGED
|
@@ -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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|