@aiquants/virtualscroll 1.18.5 → 1.19.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.
Files changed (40) hide show
  1. package/README.md +20 -13
  2. package/dist/ScrollPane.d.cts +2 -0
  3. package/dist/ScrollPane.d.ts +2 -0
  4. package/dist/ScrollPane.d.ts.map +1 -1
  5. package/dist/VirtualScroll.d.cts +2 -0
  6. package/dist/VirtualScroll.d.ts +2 -0
  7. package/dist/VirtualScroll.d.ts.map +1 -1
  8. package/dist/index.cjs +1 -1
  9. package/dist/index.d.cts +6 -0
  10. package/dist/index.d.ts +6 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1115 -1112
  13. package/dist/styles/virtualscroll.css +1 -1
  14. package/dist/styles/virtualscroll.standalone.css +3 -0
  15. package/package.json +6 -4
  16. package/src/ScrollBar.spec.tsx +620 -0
  17. package/src/ScrollBar.tsx +1397 -0
  18. package/src/ScrollPane.spec.tsx +482 -0
  19. package/src/ScrollPane.tsx +913 -0
  20. package/src/TapScrollCircle.spec.tsx +275 -0
  21. package/src/TapScrollCircle.tsx +363 -0
  22. package/src/VirtualScroll.spec.ts +623 -0
  23. package/src/VirtualScroll.tsx +1891 -0
  24. package/src/cli.server.spec.ts +137 -0
  25. package/src/cli.server.ts +110 -0
  26. package/src/index.ts +24 -0
  27. package/src/logger.spec.ts +128 -0
  28. package/src/logger.ts +229 -0
  29. package/src/styles/components.entry.css +7 -0
  30. package/src/styles/standalone.entry.css +11 -0
  31. package/src/styles/virtualscroll.css +298 -0
  32. package/src/tapScrollCircleSampleVisual.tsx +74 -0
  33. package/src/useFenwickMapTree.huge.spec.ts +388 -0
  34. package/src/useFenwickMapTree.spec.ts +1518 -0
  35. package/src/useFenwickMapTree.ts +1368 -0
  36. package/src/useHeightCache.ts +32 -0
  37. package/src/useLruCache.spec.ts +382 -0
  38. package/src/useLruCache.ts +301 -0
  39. package/src/utils.spec.ts +39 -0
  40. package/src/utils.ts +16 -0
@@ -0,0 +1,388 @@
1
+ import { act, renderHook } from "@testing-library/react"
2
+ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"
3
+ import { FenwickMapTree, useFenwickMapTree } from "./useFenwickMapTree.ts"
4
+
5
+ let warnSpy: ReturnType<typeof vi.spyOn> | undefined
6
+
7
+ beforeAll(() => {
8
+ warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
9
+ })
10
+
11
+ afterAll(() => {
12
+ warnSpy?.mockRestore()
13
+ warnSpy = undefined
14
+ })
15
+
16
+ const MAX_SAFE_BIG = BigInt(Number.MAX_SAFE_INTEGER)
17
+ const EXACT_SIZE = Number.MAX_SAFE_INTEGER
18
+ const SHRUNK_SIZE = Number(MAX_SAFE_BIG - 64n)
19
+ const HEAD_INDEX = 0
20
+ const SECOND_INDEX = 1
21
+ const MID_INDEX = Number(MAX_SAFE_BIG - 512n)
22
+ const BEFORE_MID_INDEX = MID_INDEX - 1
23
+ const PRE_TAIL_INDEX = Number(MAX_SAFE_BIG - 8n)
24
+ const BEFORE_PRE_TAIL_INDEX = PRE_TAIL_INDEX - 1
25
+ const TAIL_INDEX = Number(MAX_SAFE_BIG - 1n)
26
+ const BEFORE_TAIL_INDEX = TAIL_INDEX - 1
27
+ const SAMPLE_RANGE_NEAR_TAIL = {
28
+ from: Number(MAX_SAFE_BIG - 16n),
29
+ to: TAIL_INDEX,
30
+ }
31
+
32
+ describe("FenwickMapTree (最大安全整数近傍)", () => {
33
+ it("最大安全整数サイズで基礎的な取得ができる", () => {
34
+ const tree = new FenwickMapTree(EXACT_SIZE, 0)
35
+
36
+ expect(tree.getSize()).toBe(EXACT_SIZE)
37
+ expect(tree.getTotal()).toBe(0)
38
+ expect(tree.get(HEAD_INDEX)).toBe(0)
39
+ expect(tree.get(TAIL_INDEX)).toBe(0)
40
+ })
41
+
42
+ it("最大安全整数サイズで複数インデックスを更新しても累積和が安全に計算できる", () => {
43
+ const tree = new FenwickMapTree(EXACT_SIZE, 0)
44
+
45
+ tree.update(HEAD_INDEX, 5)
46
+ tree.update(SECOND_INDEX, 3)
47
+ tree.update(MID_INDEX, 4)
48
+ tree.update(PRE_TAIL_INDEX, 7)
49
+ tree.update(TAIL_INDEX, 9)
50
+
51
+ expect(tree.prefixSum(HEAD_INDEX).cumulative).toBe(5)
52
+ expect(tree.prefixSum(SECOND_INDEX).cumulative).toBe(8)
53
+ expect(tree.prefixSum(MID_INDEX).cumulative).toBe(12)
54
+ expect(tree.prefixSum(PRE_TAIL_INDEX).cumulative).toBe(19)
55
+ expect(tree.prefixSum(TAIL_INDEX).cumulative).toBe(28)
56
+ expect(tree.getTotal()).toBe(28)
57
+ })
58
+
59
+ it("最大安全整数サイズで findIndexAtOrAfter が閾値に対応する", () => {
60
+ const tree = new FenwickMapTree(EXACT_SIZE, 0)
61
+
62
+ tree.update(HEAD_INDEX, 5)
63
+ tree.update(SECOND_INDEX, 3)
64
+ tree.update(MID_INDEX, 4)
65
+ tree.update(PRE_TAIL_INDEX, 7)
66
+ tree.update(TAIL_INDEX, 9)
67
+
68
+ expect(tree.findIndexAtOrAfter(1).index).toBe(HEAD_INDEX)
69
+ expect(tree.findIndexAtOrAfter(8).index).toBe(SECOND_INDEX)
70
+ expect(tree.findIndexAtOrAfter(12).index).toBe(MID_INDEX)
71
+ expect(tree.findIndexAtOrAfter(19).index).toBe(PRE_TAIL_INDEX)
72
+ expect(tree.findIndexAtOrAfter(28).index).toBe(TAIL_INDEX)
73
+ expect(tree.findIndexAtOrAfter(29).index).toBe(-1)
74
+ })
75
+
76
+ it("最大安全整数サイズで findIndexAtOrBefore が閾値に対応する", () => {
77
+ const tree = new FenwickMapTree(EXACT_SIZE, 0)
78
+
79
+ tree.update(HEAD_INDEX, 5)
80
+ tree.update(SECOND_INDEX, 3)
81
+ tree.update(MID_INDEX, 4)
82
+ tree.update(PRE_TAIL_INDEX, 7)
83
+ tree.update(TAIL_INDEX, 9)
84
+
85
+ expect(tree.findIndexAtOrBefore(4).index).toBe(-1)
86
+ expect(tree.findIndexAtOrBefore(5).index).toBe(HEAD_INDEX)
87
+ expect(tree.findIndexAtOrBefore(8).index).toBe(BEFORE_MID_INDEX)
88
+ expect(tree.findIndexAtOrBefore(12).index).toBe(BEFORE_PRE_TAIL_INDEX)
89
+ expect(tree.findIndexAtOrBefore(19).index).toBe(BEFORE_TAIL_INDEX)
90
+ expect(tree.findIndexAtOrBefore(28).index).toBe(TAIL_INDEX)
91
+ })
92
+
93
+ it("巨大サイズで updates がまとめて適用される", () => {
94
+ const tree = new FenwickMapTree(EXACT_SIZE, 0)
95
+
96
+ tree.updates([
97
+ { index: HEAD_INDEX, value: 6 },
98
+ { index: MID_INDEX, value: 4 },
99
+ { index: TAIL_INDEX, value: 8 },
100
+ ])
101
+
102
+ expect(tree.get(HEAD_INDEX)).toBe(6)
103
+ expect(tree.prefixSum(MID_INDEX).cumulative).toBe(10)
104
+ expect(tree.prefixSum(TAIL_INDEX).cumulative).toBe(18)
105
+ expect(tree.getTotal()).toBe(18)
106
+ })
107
+
108
+ it("巨大サイズで updateDelta が差分を加算できる", () => {
109
+ const tree = new FenwickMapTree(EXACT_SIZE, 0)
110
+
111
+ tree.updateDelta(TAIL_INDEX, 5)
112
+ tree.updateDelta(TAIL_INDEX, 3)
113
+ tree.updateDelta(HEAD_INDEX, 4)
114
+
115
+ expect(tree.get(TAIL_INDEX)).toBe(8)
116
+ expect(tree.prefixSum(TAIL_INDEX).cumulative).toBe(12)
117
+ expect(tree.getTotal()).toBe(12)
118
+ })
119
+
120
+ it("巨大サイズで changeSize を縮小すると範囲外デルタが除去される", () => {
121
+ const tree = new FenwickMapTree(EXACT_SIZE, 0)
122
+
123
+ tree.update(HEAD_INDEX, 5)
124
+ tree.update(TAIL_INDEX, 9)
125
+
126
+ tree.changeSize(SHRUNK_SIZE)
127
+
128
+ expect(tree.getSize()).toBe(SHRUNK_SIZE)
129
+ expect(() => tree.get(TAIL_INDEX)).toThrow()
130
+ expect(tree.getTotal()).toBe(5)
131
+ })
132
+
133
+ it("巨大サイズで changeSize を拡大した後でも更新ができる", () => {
134
+ const tree = new FenwickMapTree(SHRUNK_SIZE, 0)
135
+
136
+ tree.update(HEAD_INDEX, 5)
137
+
138
+ tree.changeSize(EXACT_SIZE)
139
+ tree.update(TAIL_INDEX, 11)
140
+
141
+ expect(tree.getSize()).toBe(EXACT_SIZE)
142
+ expect(tree.get(TAIL_INDEX)).toBe(11)
143
+ expect(tree.prefixSum(TAIL_INDEX).cumulative).toBe(16)
144
+ })
145
+
146
+ it("巨大サイズで rebuildTree を呼んでも合計値が維持される", () => {
147
+ const tree = new FenwickMapTree(EXACT_SIZE, 0)
148
+
149
+ tree.update(HEAD_INDEX, 5)
150
+ tree.update(PRE_TAIL_INDEX, 7)
151
+
152
+ tree.rebuildTree()
153
+
154
+ expect(tree.get(HEAD_INDEX)).toBe(5)
155
+ expect(tree.get(PRE_TAIL_INDEX)).toBe(7)
156
+ expect(tree.getTotal()).toBe(12)
157
+ })
158
+
159
+ it("巨大サイズで valueFn による遅延具現化が機能する", () => {
160
+ const tree = new FenwickMapTree(
161
+ EXACT_SIZE,
162
+ (index) => {
163
+ if (index === HEAD_INDEX) {
164
+ return 4
165
+ }
166
+ if (index === TAIL_INDEX) {
167
+ return 6
168
+ }
169
+ return 0
170
+ },
171
+ { sampleRange: { from: 0, to: 16 }, materialize: false },
172
+ )
173
+
174
+ const head = tree.get(HEAD_INDEX, { materializeOption: { materialize: true } })
175
+ const tail = tree.get(TAIL_INDEX, { materializeOption: { materialize: true, ranges: [SAMPLE_RANGE_NEAR_TAIL] } })
176
+ const total = tree.getTotal({ materializeOption: { materialize: true, ranges: [SAMPLE_RANGE_NEAR_TAIL] } })
177
+
178
+ expect(head).toBe(4)
179
+ expect(tail).toBe(6)
180
+ expect(total).toBe(10)
181
+ })
182
+
183
+ it("巨大サイズの +1 末尾追記でも delta と total が正確 (回帰: r3 監査所見)", () => {
184
+ const tree = new FenwickMapTree(SHRUNK_SIZE, 0)
185
+ tree.update(HEAD_INDEX, 5)
186
+ tree.update(SHRUNK_SIZE - 1, 7)
187
+
188
+ tree.changeSize(SHRUNK_SIZE + 1)
189
+
190
+ expect(tree.getSize()).toBe(SHRUNK_SIZE + 1)
191
+ expect(tree.getTotal()).toBe(12)
192
+ expect(tree.prefixSum(SHRUNK_SIZE).cumulative).toBe(12)
193
+ expect(tree.get(SHRUNK_SIZE)).toBe(0)
194
+ })
195
+
196
+ it("巨大サイズ (bigint 探索パス) の findIndexAtOrAfter は materialize:true でも契約を満たし連続呼び出しで安定する (回帰: r3 監査所見)", () => {
197
+ // size >= 2^30 で _findIndexLarge (bigint 二分探索) に委譲されるパスの検証
198
+ const size = 0x80000000 // 2^31
199
+ const valueFn = (index: number) => (index === 500 ? 10 : 100)
200
+ const tree = new FenwickMapTree(size, valueFn, { sampleRange: { from: 0, to: 99 }, materialize: true })
201
+ const opts = { materializeOption: { materialize: true, ranges: [{ from: 0, to: 99 }] } }
202
+
203
+ const first = tree.findIndexAtOrAfter(50100, opts)
204
+ const second = tree.findIndexAtOrAfter(50100, opts)
205
+
206
+ // AtOrAfter 契約: cumulative は target 以上
207
+ expect(first.cumulative).toBeGreaterThanOrEqual(50100)
208
+ // 同一引数の連続呼び出しは同一結果 (探索後の木変異によるドリフトがない)
209
+ expect(second.index).toBe(first.index)
210
+ expect(second.cumulative).toBe(first.cumulative)
211
+ })
212
+
213
+ it("巨大サイズ (bigint 探索パス) の未発見時は小サイズパスと同一契約で cumulative などが undefined になる (回帰: r4 監査所見)", () => {
214
+ // 旧実装は bestResult ?? lastResult のフォールバックにより、未発見 (index=-1) でも
215
+ // 最後にプローブした位置の cumulative/currentValue/safeIndex が載っていた
216
+ // (AtOrBefore の未発見時は cumulative > target という契約矛盾の値になる)。
217
+ const size = 0x40000000 // 2^30 (bigint 探索パスの下限)
218
+ const tree = new FenwickMapTree(size, 10)
219
+
220
+ // AtOrAfter: total を超える target は未発見
221
+ const after = tree.findIndexAtOrAfter(Number.MAX_SAFE_INTEGER)
222
+ expect(after.index).toBe(-1)
223
+ expect(after.cumulative).toBeUndefined()
224
+ expect(after.currentValue).toBeUndefined()
225
+ expect(after.safeIndex).toBeUndefined()
226
+
227
+ // AtOrBefore: どの累積和よりも小さい target は未発見
228
+ const before = tree.findIndexAtOrBefore(-1)
229
+ expect(before.index).toBe(-1)
230
+ expect(before.cumulative).toBeUndefined()
231
+ expect(before.currentValue).toBeUndefined()
232
+ expect(before.safeIndex).toBeUndefined()
233
+
234
+ // 小サイズパスと同一契約であることの対照確認
235
+ const small = new FenwickMapTree(1000, 10)
236
+ const smallAfter = small.findIndexAtOrAfter(Number.MAX_SAFE_INTEGER)
237
+ expect(smallAfter.index).toBe(-1)
238
+ expect(smallAfter.cumulative).toBeUndefined()
239
+ expect(smallAfter.safeIndex).toBeUndefined()
240
+ })
241
+
242
+ it("巨大サイズで materialize 範囲を指定して prefixSum が反映される", () => {
243
+ const tree = new FenwickMapTree(
244
+ EXACT_SIZE,
245
+ (index) => {
246
+ if (index === PRE_TAIL_INDEX) {
247
+ return 7
248
+ }
249
+ if (index === TAIL_INDEX) {
250
+ return 5
251
+ }
252
+ return 0
253
+ },
254
+ { sampleRange: SAMPLE_RANGE_NEAR_TAIL, materialize: false },
255
+ )
256
+
257
+ const prefix = tree.prefixSum(TAIL_INDEX, { materializeOption: { materialize: true, ranges: [SAMPLE_RANGE_NEAR_TAIL] } })
258
+
259
+ expect(prefix.currentValue).toBe(5)
260
+ expect(prefix.cumulative).toBe(12)
261
+ expect(prefix.total).toBe(12)
262
+ })
263
+ })
264
+
265
+ describe("useFenwickMapTree (最大安全整数近傍)", () => {
266
+ it("最大安全整数サイズで同一パラメーターならインスタンスが安定する", () => {
267
+ const { result, rerender } = renderHook<FenwickMapTree, { size: number; value: number | ((index: number) => number) }>(({ size, value }) => useFenwickMapTree(size, value), {
268
+ initialProps: { size: EXACT_SIZE, value: 0 },
269
+ })
270
+
271
+ const first = result.current
272
+
273
+ rerender({ size: EXACT_SIZE, value: 0 })
274
+
275
+ expect(result.current).toBe(first)
276
+ })
277
+
278
+ it("最大安全整数サイズでサイズが変わっても同じインスタンスになる(安定性保証)", () => {
279
+ const { result, rerender } = renderHook<FenwickMapTree, { size: number; value: number | ((index: number) => number) }>(({ size, value }) => useFenwickMapTree(size, value), {
280
+ initialProps: { size: EXACT_SIZE, value: 0 },
281
+ })
282
+
283
+ const first = result.current
284
+
285
+ rerender({ size: SHRUNK_SIZE, value: 0 })
286
+
287
+ expect(result.current).toBe(first)
288
+ })
289
+
290
+ it("最大安全整数サイズで valueFn が変わっても同じインスタンスになる(安定性保証)", () => {
291
+ const { result, rerender } = renderHook<FenwickMapTree, { size: number; value: number | ((index: number) => number) }>(({ size, value }) => useFenwickMapTree(size, value), {
292
+ initialProps: { size: EXACT_SIZE, value: 0 },
293
+ })
294
+
295
+ const first = result.current
296
+
297
+ rerender({
298
+ size: EXACT_SIZE,
299
+ value: (index: number) => {
300
+ if (index === HEAD_INDEX) {
301
+ return 2
302
+ }
303
+ return 0
304
+ },
305
+ })
306
+
307
+ expect(result.current).toBe(first)
308
+ })
309
+
310
+ it("最大安全整数サイズで返却されたツリーが更新を反映する", () => {
311
+ const { result } = renderHook(() => useFenwickMapTree(EXACT_SIZE, 0))
312
+
313
+ act(() => {
314
+ result.current.update(HEAD_INDEX, 5)
315
+ result.current.update(TAIL_INDEX, 9)
316
+ })
317
+
318
+ expect(result.current.get(HEAD_INDEX)).toBe(5)
319
+ expect(result.current.get(TAIL_INDEX)).toBe(9)
320
+ expect(result.current.prefixSum(TAIL_INDEX).cumulative).toBe(14)
321
+ })
322
+ })
323
+
324
+ describe("FenwickMapTree (100 Billion items & 1 Trillion items)", () => {
325
+ const HUNDRED_BILLION = 100_000_000_000
326
+
327
+ it("works without infinite loop for 100 Billion items", () => {
328
+ // Initialize with default item size 20
329
+ const tree = new FenwickMapTree(HUNDRED_BILLION, 20)
330
+
331
+ // This operation triggers _findIndex or _findIndexLarge
332
+ // Before fix: loop around 2^31 -> hang
333
+ // After fix: uses _findIndexLarge -> success
334
+
335
+ // Target 1000.
336
+ // Index 49: cumulative = (49+1)*20 = 1000. 1000 >= 1000. Found.
337
+ const targetOffset = 20 * 50
338
+ const result = tree.findIndexAtOrAfter(targetOffset)
339
+
340
+ expect(result.index).toBe(49)
341
+ expect(result.cumulative).toBe(targetOffset)
342
+ })
343
+
344
+ it("works for size just related to bitwise boundary (3 Billion)", () => {
345
+ const THREE_BILLION = 3_000_000_000
346
+ const tree = new FenwickMapTree(THREE_BILLION, 10)
347
+ // 10 * 60 = 600.
348
+ // Index 59: (59+1)*10 = 600.
349
+ const result = tree.findIndexAtOrAfter(10 * 60)
350
+ expect(result.index).toBe(59)
351
+ })
352
+
353
+ it("(a) does not hang at the 2^30 bitwise-doubling boundary", () => {
354
+ // size = 2^30 はビット倍加ループが Int32 オーバーフローする中間域。
355
+ // 修正前: bit << 1 が -2^31 に化けて無限ループ (タイムアウト)。
356
+ const tree = new FenwickMapTree(0x40000000, 10) // 2^30 = 1,073,741,824
357
+ // index 9: cumulative = (9+1)*10 = 100 >= 100
358
+ const result = tree.findIndexAtOrAfter(100)
359
+ expect(result.index).toBe(9)
360
+ expect(result.cumulative).toBe(100)
361
+ })
362
+
363
+ it("(a) does not hang for sizes in [2^30, 2^31-1] (max 32-bit signed)", () => {
364
+ const tree = new FenwickMapTree(0x7fffffff, 10) // 2^31 - 1 = 2,147,483,647
365
+ const result = tree.findIndexAtOrAfter(100)
366
+ expect(result.index).toBe(9)
367
+ expect(result.cumulative).toBe(100)
368
+
369
+ const before = tree.findIndexAtOrBefore(100)
370
+ expect(before.index).toBe(9)
371
+ })
372
+
373
+ it("works for 1 Trillion items (1,000,000,000,000)", () => {
374
+ const TRILLION = 1_000_000_000_000
375
+ // Need to ensure total height fits in safe integer.
376
+ // MAX safe integer ~9 Quadrillion.
377
+ // 1 Trillion * 25 = 25 Trillion on safe side.
378
+ const itemSize = 25
379
+ const tree = new FenwickMapTree(TRILLION, itemSize)
380
+
381
+ const indexToFind = 500_000_000_000 // 500 Billion
382
+ const targetOffset = itemSize * (indexToFind + 1)
383
+ const result = tree.findIndexAtOrAfter(targetOffset)
384
+
385
+ expect(result.index).toBe(indexToFind)
386
+ expect(result.cumulative).toBe(targetOffset)
387
+ })
388
+ })