@humanspeak/svelte-virtual-list 0.2.6 → 0.3.1-beta.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/README.md +50 -13
- package/dist/SvelteVirtualList.svelte +619 -179
- package/dist/SvelteVirtualList.svelte.d.ts +156 -65
- package/dist/reactive-height-manager/INTEGRATION_EXAMPLE.md +136 -0
- package/dist/reactive-height-manager/README.md +324 -0
- package/dist/reactive-height-manager/ReactiveHeightManager.svelte.d.ts +116 -0
- package/dist/reactive-height-manager/ReactiveHeightManager.svelte.js +200 -0
- package/dist/reactive-height-manager/benchmark.d.ts +5 -0
- package/dist/reactive-height-manager/benchmark.js +25 -0
- package/dist/reactive-height-manager/index.d.ts +50 -0
- package/dist/reactive-height-manager/index.js +55 -0
- package/dist/reactive-height-manager/test/TestComponent.svelte +78 -0
- package/dist/reactive-height-manager/test/TestComponent.svelte.d.ts +23 -0
- package/dist/reactive-height-manager/types.d.ts +41 -0
- package/dist/reactive-height-manager/types.js +1 -0
- package/dist/types.d.ts +24 -5
- package/dist/utils/heightCalculation.d.ts +18 -8
- package/dist/utils/heightCalculation.js +18 -11
- package/dist/utils/heightChangeDetection.d.ts +12 -0
- package/dist/utils/heightChangeDetection.js +20 -0
- package/dist/utils/resizeObserver.d.ts +89 -0
- package/dist/utils/resizeObserver.js +119 -0
- package/dist/utils/scrollCalculation.d.ts +47 -0
- package/dist/utils/scrollCalculation.js +167 -0
- package/dist/utils/throttle.d.ts +95 -0
- package/dist/utils/throttle.js +155 -0
- package/dist/utils/types.d.ts +0 -6
- package/dist/utils/virtualList.d.ts +20 -23
- package/dist/utils/virtualList.js +153 -61
- package/dist/utils/virtualListDebug.d.ts +12 -7
- package/dist/utils/virtualListDebug.js +19 -9
- package/package.json +33 -31
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# ReactiveHeightManager
|
|
2
|
+
|
|
3
|
+
> A standalone, high-performance reactive height calculation system for virtualized lists
|
|
4
|
+
|
|
5
|
+
<!-- [](./ReactiveHeightManager.test.ts)
|
|
6
|
+
[](#performance)
|
|
7
|
+
[](https://www.typescriptlang.org/) -->
|
|
8
|
+
|
|
9
|
+
## 🚀 Features
|
|
10
|
+
|
|
11
|
+
- **Incremental Processing**: O(dirty items) instead of O(all items)
|
|
12
|
+
- **Reactive State**: Built with Svelte 5 runes for automatic updates
|
|
13
|
+
- **Framework Agnostic**: Standalone types, no external dependencies
|
|
14
|
+
- **High Performance**: 1000+ height updates processed in <1ms
|
|
15
|
+
- **Memory Efficient**: Tracks only measured vs estimated items
|
|
16
|
+
- **Comprehensive Testing**: 13 test cases including performance benchmarks
|
|
17
|
+
|
|
18
|
+
## 🎯 Problem It Solves
|
|
19
|
+
|
|
20
|
+
Traditional virtual list height calculations loop through **all items** on every change:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// ❌ O(n) - Slow for large lists
|
|
24
|
+
let totalHeight = 0
|
|
25
|
+
for (let i = 0; i < items.length; i++) {
|
|
26
|
+
totalHeight += heightCache[i] || estimatedHeight
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
ReactiveHeightManager processes only **dirty/changed items**:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// ✅ O(dirty items) - Fast and reactive
|
|
34
|
+
manager.processDirtyHeights(changedItems)
|
|
35
|
+
const totalHeight = manager.getDerivedTotalHeight()
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 📦 Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# If using within svelte-virtual-list project
|
|
42
|
+
import { ReactiveHeightManager } from '$lib/reactive-height-manager'
|
|
43
|
+
|
|
44
|
+
# For standalone usage (copy the module)
|
|
45
|
+
cp -r src/lib/reactive-height-manager your-project/src/lib/
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 🚀 Quick Start
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { ReactiveHeightManager } from './reactive-height-manager'
|
|
52
|
+
|
|
53
|
+
// Create manager
|
|
54
|
+
const manager = new ReactiveHeightManager({
|
|
55
|
+
itemLength: 10000,
|
|
56
|
+
itemHeight: 40
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Process height changes
|
|
60
|
+
const heightChanges = [
|
|
61
|
+
{ index: 0, oldHeight: undefined, newHeight: 45 },
|
|
62
|
+
{ index: 1, oldHeight: 40, newHeight: 50 }
|
|
63
|
+
]
|
|
64
|
+
manager.processDirtyHeights(heightChanges)
|
|
65
|
+
|
|
66
|
+
// Update calculated item height
|
|
67
|
+
manager.calculatedItemHeight = 42
|
|
68
|
+
|
|
69
|
+
// Get reactive total height (automatically updates)
|
|
70
|
+
const totalHeight = manager.totalHeight
|
|
71
|
+
console.log(`Total height: ${totalHeight}px`)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 📖 API Documentation
|
|
75
|
+
|
|
76
|
+
### Constructor
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
new ReactiveHeightManager(config: HeightManagerConfig)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Parameters:**
|
|
83
|
+
|
|
84
|
+
- `config.itemLength` - Total number of items
|
|
85
|
+
- `config.estimatedHeight` - Default height for unmeasured items
|
|
86
|
+
|
|
87
|
+
### Core Methods
|
|
88
|
+
|
|
89
|
+
#### `processDirtyHeights(changes: HeightChange[])`
|
|
90
|
+
|
|
91
|
+
Process height changes incrementally. This is the performance-critical method.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
const changes = [{ index: 0, oldHeight: undefined, newHeight: 45 }]
|
|
95
|
+
manager.processDirtyHeights(changes)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### `totalHeight` (getter)
|
|
99
|
+
|
|
100
|
+
Get total height of all items (measured + estimated). This property is reactive and automatically updates when dependencies change.
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const totalHeight = manager.totalHeight // Automatically reactive
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### `calculatedItemHeight` (getter/setter)
|
|
107
|
+
|
|
108
|
+
Get or set the calculated average item height, which affects total height calculations.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
manager.calculatedItemHeight = 42 // Updates totalHeight automatically
|
|
112
|
+
const currentHeight = manager.calculatedItemHeight
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### State Management
|
|
116
|
+
|
|
117
|
+
#### `updateItemLength(newLength: number)`
|
|
118
|
+
|
|
119
|
+
Update when items array changes.
|
|
120
|
+
|
|
121
|
+
#### `updateEstimatedHeight(newHeight: number)`
|
|
122
|
+
|
|
123
|
+
Update estimated height for unmeasured items.
|
|
124
|
+
|
|
125
|
+
#### `reset()`
|
|
126
|
+
|
|
127
|
+
Reset all state to initial values.
|
|
128
|
+
|
|
129
|
+
### Utilities
|
|
130
|
+
|
|
131
|
+
#### `getDebugInfo(): HeightManagerDebugInfo`
|
|
132
|
+
|
|
133
|
+
Get comprehensive debug information.
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
const debug = manager.getDebugInfo()
|
|
137
|
+
console.log(`Coverage: ${debug.coveragePercent}%`)
|
|
138
|
+
console.log(`Measured: ${debug.measuredCount}/${debug.itemLength}`)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### `getMeasurementCoverage(): number`
|
|
142
|
+
|
|
143
|
+
Get percentage of items measured (0-100).
|
|
144
|
+
|
|
145
|
+
#### `hasSufficientMeasurements(threshold?: number): boolean`
|
|
146
|
+
|
|
147
|
+
Check if manager has sufficient measurement data.
|
|
148
|
+
|
|
149
|
+
## 🎨 Integration Examples
|
|
150
|
+
|
|
151
|
+
### With SvelteVirtualList
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// Create manager
|
|
155
|
+
const heightManager = new ReactiveHeightManager({
|
|
156
|
+
itemLength: items.length,
|
|
157
|
+
estimatedHeight: defaultEstimatedItemHeight
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// Update on items change
|
|
161
|
+
$effect(() => {
|
|
162
|
+
heightManager.updateItemLength(items.length)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// Process in callback
|
|
166
|
+
const updateHeight = () => {
|
|
167
|
+
heightUpdateTimeout = calculateAverageHeightDebounced(
|
|
168
|
+
// ... params
|
|
169
|
+
(result) => {
|
|
170
|
+
// Convert types if needed (or pass directly if compatible)
|
|
171
|
+
const heightChanges = result.heightChanges
|
|
172
|
+
|
|
173
|
+
// Process incrementally
|
|
174
|
+
heightManager.processDirtyHeights(heightChanges)
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Update calculated height when needed
|
|
180
|
+
$effect(() => {
|
|
181
|
+
heightManager.calculatedItemHeight = calculatedItemHeight
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// Reactive total height (automatically updates)
|
|
185
|
+
let totalHeight = $derived(() => heightManager.totalHeight)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Standalone Usage
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { ReactiveHeightManager, benchmarkHeightManager } from './reactive-height-manager'
|
|
192
|
+
|
|
193
|
+
const manager = new ReactiveHeightManager({ itemLength: 1000, estimatedHeight: 50 })
|
|
194
|
+
|
|
195
|
+
// Performance monitoring
|
|
196
|
+
const results = benchmarkHeightManager(10000, 1000, 100)
|
|
197
|
+
console.log(`Average time: ${results.avgTime.toFixed(2)}ms`)
|
|
198
|
+
console.log(`Operations/sec: ${results.opsPerSecond.toFixed(0)}`)
|
|
199
|
+
|
|
200
|
+
// Test reactive totalHeight
|
|
201
|
+
manager.calculatedItemHeight = 50 // Triggers reactive update
|
|
202
|
+
console.log(`New total height: ${manager.totalHeight}`)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## ⚡ Performance
|
|
206
|
+
|
|
207
|
+
### Benchmarks
|
|
208
|
+
|
|
209
|
+
| Operation | Items | Time | Performance |
|
|
210
|
+
| -------------------- | ------ | ------ | ------------ |
|
|
211
|
+
| 1,000 dirty updates | 10,000 | < 1ms | 🚀 Excellent |
|
|
212
|
+
| 10,000 dirty updates | 10,000 | < 10ms | 🚀 Excellent |
|
|
213
|
+
| Complex scenarios | 5,000 | < 25ms | ✅ Good |
|
|
214
|
+
|
|
215
|
+
### Memory Usage
|
|
216
|
+
|
|
217
|
+
- **Measured Items**: Tracked incrementally
|
|
218
|
+
- **Unmeasured Items**: Single multiplier calculation
|
|
219
|
+
- **State**: Only 4 reactive variables total
|
|
220
|
+
|
|
221
|
+
### Compared to O(n) Loop
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
// Before: O(n) calculation every time
|
|
225
|
+
for (let i = 0; i < 100000; i++) {
|
|
226
|
+
/* ... */
|
|
227
|
+
} // ~10-50ms
|
|
228
|
+
|
|
229
|
+
// After: O(1) reactive access
|
|
230
|
+
manager.totalHeight // ~0.01ms
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## 🧪 Testing
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
# Run all tests
|
|
237
|
+
npm run test -- ReactiveHeightManager.test.ts
|
|
238
|
+
|
|
239
|
+
# Verbose output
|
|
240
|
+
npm run test -- ReactiveHeightManager.test.ts --reporter=verbose
|
|
241
|
+
|
|
242
|
+
# Performance benchmarking
|
|
243
|
+
npm run test -- --grep "Performance Tests"
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Test Coverage
|
|
247
|
+
|
|
248
|
+
- ✅ **Initialization** (2 tests)
|
|
249
|
+
- ✅ **Performance** (3 tests) - Sub-millisecond operations
|
|
250
|
+
- ✅ **Accuracy** (2 tests) - Complex scenarios with alternating heights
|
|
251
|
+
- ✅ **State Management** (3 tests) - Updates, resets, configuration
|
|
252
|
+
- ✅ **Utilities** (3 tests) - Coverage tracking, debug info
|
|
253
|
+
|
|
254
|
+
## 🏗️ Architecture
|
|
255
|
+
|
|
256
|
+
### Core Principles
|
|
257
|
+
|
|
258
|
+
1. **Reactive State**: Uses Svelte 5 `$state` runes
|
|
259
|
+
2. **Incremental Processing**: Only process changed items
|
|
260
|
+
3. **Memory Efficiency**: Track totals, not individual measurements
|
|
261
|
+
4. **Type Safety**: Comprehensive TypeScript interfaces
|
|
262
|
+
|
|
263
|
+
### Data Flow
|
|
264
|
+
|
|
265
|
+
```text
|
|
266
|
+
|
|
267
|
+
Height Changes → processDirtyHeights() → Update State → getDerivedTotalHeight() → Reactive UI
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Internal State
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
private _totalMeasuredHeight = $state(0) // Sum of all measured heights
|
|
275
|
+
private _measuredCount = $state(0) // Count of measured items
|
|
276
|
+
private _itemLength = $state(0) // Total items
|
|
277
|
+
private _estimatedHeight = $state(40) // Default estimate
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## 🔧 Types
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
interface HeightChange {
|
|
284
|
+
readonly index: number
|
|
285
|
+
readonly oldHeight: number | undefined
|
|
286
|
+
readonly newHeight: number
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
interface HeightManagerConfig {
|
|
290
|
+
itemLength: number
|
|
291
|
+
estimatedHeight: number
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
interface HeightManagerDebugInfo {
|
|
295
|
+
totalMeasuredHeight: number
|
|
296
|
+
measuredCount: number
|
|
297
|
+
itemLength: number
|
|
298
|
+
coveragePercent: number
|
|
299
|
+
estimatedHeight: number
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## 📈 Roadmap
|
|
304
|
+
|
|
305
|
+
- [ ] Batch processing for very large change sets
|
|
306
|
+
- [ ] Configurable block-based calculations
|
|
307
|
+
- [ ] Export as standalone npm package
|
|
308
|
+
- [ ] React/Vue adapter examples
|
|
309
|
+
- [ ] Web Worker support for massive datasets
|
|
310
|
+
|
|
311
|
+
## 🤝 Contributing
|
|
312
|
+
|
|
313
|
+
1. Run tests: `npm run test -- ReactiveHeightManager.test.ts`
|
|
314
|
+
2. Add performance benchmarks for new features
|
|
315
|
+
3. Maintain O(1) or O(dirty) complexity
|
|
316
|
+
4. Update type definitions
|
|
317
|
+
|
|
318
|
+
## 📄 License
|
|
319
|
+
|
|
320
|
+
Part of the svelte-virtual-list project. See main [LICENSE](../../../LICENSE) file.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
**Need help?** Check the [Integration Examples](./INTEGRATION_EXAMPLE.md) for detailed usage patterns.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { HeightChange, HeightManagerConfig, HeightManagerDebugInfo } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* ReactiveHeightManager - A standalone reactive height calculation system
|
|
4
|
+
*
|
|
5
|
+
* Efficiently manages height calculations for virtualized lists by:
|
|
6
|
+
* - Tracking measured vs unmeasured items incrementally
|
|
7
|
+
* - Processing only dirty/changed items (O(dirty) instead of O(all))
|
|
8
|
+
* - Providing reactive state updates using Svelte 5 runes
|
|
9
|
+
* - Maintaining accurate total height calculations
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const manager = new ReactiveHeightManager({ itemLength: 10000, estimatedHeight: 40 })
|
|
14
|
+
*
|
|
15
|
+
* // Process height changes incrementally
|
|
16
|
+
* manager.processDirtyHeights(dirtyResults)
|
|
17
|
+
*
|
|
18
|
+
* // Update calculated item height
|
|
19
|
+
* manager.calculatedItemHeight = 42
|
|
20
|
+
*
|
|
21
|
+
* // Get reactive total height (automatically updates)
|
|
22
|
+
* const totalHeight = manager.totalHeight
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare class ReactiveHeightManager {
|
|
26
|
+
private _totalMeasuredHeight;
|
|
27
|
+
private _measuredCount;
|
|
28
|
+
private _itemLength;
|
|
29
|
+
private _itemHeight;
|
|
30
|
+
private _averageHeight;
|
|
31
|
+
private _totalHeight;
|
|
32
|
+
private _measuredFlags;
|
|
33
|
+
private recomputeDerivedHeights;
|
|
34
|
+
/**
|
|
35
|
+
* Get total measured height of all measured items
|
|
36
|
+
*/
|
|
37
|
+
get totalMeasuredHeight(): number;
|
|
38
|
+
/**
|
|
39
|
+
* Get count of items that have been measured
|
|
40
|
+
*/
|
|
41
|
+
get measuredCount(): number;
|
|
42
|
+
/**
|
|
43
|
+
* Get total number of items in the list
|
|
44
|
+
*/
|
|
45
|
+
get itemLength(): number;
|
|
46
|
+
/**
|
|
47
|
+
* Get/Set the height to use for unmeasured items (reactive)
|
|
48
|
+
*/
|
|
49
|
+
get itemHeight(): number;
|
|
50
|
+
set itemHeight(value: number);
|
|
51
|
+
/**
|
|
52
|
+
* Get the calculated average height of measured items
|
|
53
|
+
* Falls back to itemHeight if no items have been measured yet
|
|
54
|
+
*/
|
|
55
|
+
get averageHeight(): number;
|
|
56
|
+
/**
|
|
57
|
+
* Get the reactive total height of all items (measured + estimated)
|
|
58
|
+
* This automatically updates when any dependencies change
|
|
59
|
+
*/
|
|
60
|
+
get totalHeight(): number;
|
|
61
|
+
/**
|
|
62
|
+
* Create a new ReactiveHeightManager instance
|
|
63
|
+
*
|
|
64
|
+
* @param config - Configuration object containing itemLength and itemHeight
|
|
65
|
+
*/
|
|
66
|
+
constructor(config: HeightManagerConfig);
|
|
67
|
+
/**
|
|
68
|
+
* Process height changes incrementally - O(dirty items) instead of O(all items)
|
|
69
|
+
*
|
|
70
|
+
* This is the core optimization: instead of recalculating totals for all items,
|
|
71
|
+
* we only process the items that have changed, maintaining running totals.
|
|
72
|
+
*
|
|
73
|
+
* Accepts any object that has index, oldHeight, and newHeight properties,
|
|
74
|
+
* allowing consumers to pass objects with additional fields.
|
|
75
|
+
*
|
|
76
|
+
* @param dirtyResults - Array of height changes to process
|
|
77
|
+
*/
|
|
78
|
+
processDirtyHeights(dirtyResults: HeightChange[]): void;
|
|
79
|
+
/**
|
|
80
|
+
* Update when items array length changes
|
|
81
|
+
*
|
|
82
|
+
* @param newLength - New total number of items
|
|
83
|
+
*/
|
|
84
|
+
updateItemLength(newLength: number): void;
|
|
85
|
+
/**
|
|
86
|
+
* Update estimated height for unmeasured items
|
|
87
|
+
*
|
|
88
|
+
* @param newEstimatedHeight - New estimated height
|
|
89
|
+
*/
|
|
90
|
+
updateEstimatedHeight(newEstimatedHeight: number): void;
|
|
91
|
+
/**
|
|
92
|
+
* Reset all state to initial values
|
|
93
|
+
*
|
|
94
|
+
* Useful for testing or when completely reinitializing the list
|
|
95
|
+
*/
|
|
96
|
+
reset(): void;
|
|
97
|
+
/**
|
|
98
|
+
* Get comprehensive debug information
|
|
99
|
+
*
|
|
100
|
+
* @returns Debug information object
|
|
101
|
+
*/
|
|
102
|
+
getDebugInfo(): HeightManagerDebugInfo;
|
|
103
|
+
/**
|
|
104
|
+
* Get the percentage of items that have been measured
|
|
105
|
+
*
|
|
106
|
+
* @returns Percentage (0-100) of measured items
|
|
107
|
+
*/
|
|
108
|
+
getMeasurementCoverage(): number;
|
|
109
|
+
/**
|
|
110
|
+
* Check if the manager has sufficient measurement data
|
|
111
|
+
*
|
|
112
|
+
* @param threshold - Minimum percentage of items that should be measured (default: 10)
|
|
113
|
+
* @returns true if coverage meets threshold
|
|
114
|
+
*/
|
|
115
|
+
hasSufficientMeasurements(threshold?: number): boolean;
|
|
116
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReactiveHeightManager - A standalone reactive height calculation system
|
|
3
|
+
*
|
|
4
|
+
* Efficiently manages height calculations for virtualized lists by:
|
|
5
|
+
* - Tracking measured vs unmeasured items incrementally
|
|
6
|
+
* - Processing only dirty/changed items (O(dirty) instead of O(all))
|
|
7
|
+
* - Providing reactive state updates using Svelte 5 runes
|
|
8
|
+
* - Maintaining accurate total height calculations
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const manager = new ReactiveHeightManager({ itemLength: 10000, estimatedHeight: 40 })
|
|
13
|
+
*
|
|
14
|
+
* // Process height changes incrementally
|
|
15
|
+
* manager.processDirtyHeights(dirtyResults)
|
|
16
|
+
*
|
|
17
|
+
* // Update calculated item height
|
|
18
|
+
* manager.calculatedItemHeight = 42
|
|
19
|
+
*
|
|
20
|
+
* // Get reactive total height (automatically updates)
|
|
21
|
+
* const totalHeight = manager.totalHeight
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export class ReactiveHeightManager {
|
|
25
|
+
// Reactive state using Svelte 5 runes
|
|
26
|
+
_totalMeasuredHeight = $state(0);
|
|
27
|
+
_measuredCount = $state(0);
|
|
28
|
+
_itemLength = $state(0);
|
|
29
|
+
_itemHeight = $state(40);
|
|
30
|
+
_averageHeight = $state(40);
|
|
31
|
+
_totalHeight = $state(0);
|
|
32
|
+
_measuredFlags = null;
|
|
33
|
+
recomputeDerivedHeights() {
|
|
34
|
+
const average = this._measuredCount > 0
|
|
35
|
+
? this._totalMeasuredHeight / this._measuredCount
|
|
36
|
+
: this._itemHeight;
|
|
37
|
+
this._averageHeight = average;
|
|
38
|
+
const unmeasuredCount = this._itemLength - this._measuredCount;
|
|
39
|
+
this._totalHeight = this._totalMeasuredHeight + unmeasuredCount * average;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get total measured height of all measured items
|
|
43
|
+
*/
|
|
44
|
+
get totalMeasuredHeight() {
|
|
45
|
+
return this._totalMeasuredHeight;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get count of items that have been measured
|
|
49
|
+
*/
|
|
50
|
+
get measuredCount() {
|
|
51
|
+
return this._measuredCount;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get total number of items in the list
|
|
55
|
+
*/
|
|
56
|
+
get itemLength() {
|
|
57
|
+
return this._itemLength;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get/Set the height to use for unmeasured items (reactive)
|
|
61
|
+
*/
|
|
62
|
+
get itemHeight() {
|
|
63
|
+
return this._itemHeight;
|
|
64
|
+
}
|
|
65
|
+
set itemHeight(value) {
|
|
66
|
+
this._itemHeight = value;
|
|
67
|
+
this.recomputeDerivedHeights();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get the calculated average height of measured items
|
|
71
|
+
* Falls back to itemHeight if no items have been measured yet
|
|
72
|
+
*/
|
|
73
|
+
get averageHeight() {
|
|
74
|
+
return this._averageHeight;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get the reactive total height of all items (measured + estimated)
|
|
78
|
+
* This automatically updates when any dependencies change
|
|
79
|
+
*/
|
|
80
|
+
get totalHeight() {
|
|
81
|
+
return this._totalHeight;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Create a new ReactiveHeightManager instance
|
|
85
|
+
*
|
|
86
|
+
* @param config - Configuration object containing itemLength and itemHeight
|
|
87
|
+
*/
|
|
88
|
+
constructor(config) {
|
|
89
|
+
this._itemLength = config.itemLength;
|
|
90
|
+
this._itemHeight = config.itemHeight;
|
|
91
|
+
this._measuredFlags = new Uint8Array(Math.max(0, this._itemLength));
|
|
92
|
+
this.recomputeDerivedHeights();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Process height changes incrementally - O(dirty items) instead of O(all items)
|
|
96
|
+
*
|
|
97
|
+
* This is the core optimization: instead of recalculating totals for all items,
|
|
98
|
+
* we only process the items that have changed, maintaining running totals.
|
|
99
|
+
*
|
|
100
|
+
* Accepts any object that has index, oldHeight, and newHeight properties,
|
|
101
|
+
* allowing consumers to pass objects with additional fields.
|
|
102
|
+
*
|
|
103
|
+
* @param dirtyResults - Array of height changes to process
|
|
104
|
+
*/
|
|
105
|
+
processDirtyHeights(dirtyResults) {
|
|
106
|
+
if (dirtyResults.length === 0)
|
|
107
|
+
return;
|
|
108
|
+
// Batch calculate changes to trigger reactivity only once
|
|
109
|
+
let heightDelta = 0;
|
|
110
|
+
let countDelta = 0;
|
|
111
|
+
for (const change of dirtyResults) {
|
|
112
|
+
const { index, oldHeight, newHeight } = change;
|
|
113
|
+
// Remove old contribution if it existed
|
|
114
|
+
if (oldHeight !== undefined) {
|
|
115
|
+
heightDelta -= oldHeight;
|
|
116
|
+
countDelta -= 1;
|
|
117
|
+
}
|
|
118
|
+
// Add new contribution
|
|
119
|
+
if (newHeight !== undefined) {
|
|
120
|
+
heightDelta += newHeight;
|
|
121
|
+
countDelta += 1;
|
|
122
|
+
}
|
|
123
|
+
// Track measured flag (best-effort; full coalescing handled separately)
|
|
124
|
+
if (this._measuredFlags && index >= 0 && index < this._measuredFlags.length) {
|
|
125
|
+
this._measuredFlags[index] = 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (heightDelta === 0 && countDelta === 0)
|
|
129
|
+
return;
|
|
130
|
+
// Apply all changes at once - triggers reactivity only once
|
|
131
|
+
this._totalMeasuredHeight += heightDelta;
|
|
132
|
+
this._measuredCount += countDelta;
|
|
133
|
+
this.recomputeDerivedHeights();
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Update when items array length changes
|
|
137
|
+
*
|
|
138
|
+
* @param newLength - New total number of items
|
|
139
|
+
*/
|
|
140
|
+
updateItemLength(newLength) {
|
|
141
|
+
this._itemLength = newLength;
|
|
142
|
+
this._measuredFlags = new Uint8Array(Math.max(0, newLength));
|
|
143
|
+
this.recomputeDerivedHeights();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Update estimated height for unmeasured items
|
|
147
|
+
*
|
|
148
|
+
* @param newEstimatedHeight - New estimated height
|
|
149
|
+
*/
|
|
150
|
+
updateEstimatedHeight(newEstimatedHeight) {
|
|
151
|
+
// Keep a single source of truth for the estimated height
|
|
152
|
+
this._itemHeight = newEstimatedHeight;
|
|
153
|
+
this.recomputeDerivedHeights();
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Reset all state to initial values
|
|
157
|
+
*
|
|
158
|
+
* Useful for testing or when completely reinitializing the list
|
|
159
|
+
*/
|
|
160
|
+
reset() {
|
|
161
|
+
this._totalMeasuredHeight = 0;
|
|
162
|
+
this._measuredCount = 0;
|
|
163
|
+
this._measuredFlags = this._itemLength > 0 ? new Uint8Array(this._itemLength) : null;
|
|
164
|
+
// Note: Don't reset _itemLength, _itemHeight as they represent configuration, not measured state
|
|
165
|
+
this.recomputeDerivedHeights();
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Get comprehensive debug information
|
|
169
|
+
*
|
|
170
|
+
* @returns Debug information object
|
|
171
|
+
*/
|
|
172
|
+
getDebugInfo() {
|
|
173
|
+
return {
|
|
174
|
+
totalMeasuredHeight: this._totalMeasuredHeight,
|
|
175
|
+
measuredCount: this._measuredCount,
|
|
176
|
+
itemLength: this._itemLength,
|
|
177
|
+
coveragePercent: this._itemLength > 0 ? (this._measuredCount / this._itemLength) * 100 : 0,
|
|
178
|
+
itemHeight: this._itemHeight,
|
|
179
|
+
averageHeight: this.averageHeight,
|
|
180
|
+
totalHeight: this.totalHeight
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get the percentage of items that have been measured
|
|
185
|
+
*
|
|
186
|
+
* @returns Percentage (0-100) of measured items
|
|
187
|
+
*/
|
|
188
|
+
getMeasurementCoverage() {
|
|
189
|
+
return this.getDebugInfo().coveragePercent;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Check if the manager has sufficient measurement data
|
|
193
|
+
*
|
|
194
|
+
* @param threshold - Minimum percentage of items that should be measured (default: 10)
|
|
195
|
+
* @returns true if coverage meets threshold
|
|
196
|
+
*/
|
|
197
|
+
hasSufficientMeasurements(threshold = 10) {
|
|
198
|
+
return this.getMeasurementCoverage() >= threshold;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createHeightManager } from './index.js';
|
|
2
|
+
export function benchmarkHeightManager(itemCount, dirtyCount, iterations = 100) {
|
|
3
|
+
const manager = createHeightManager(itemCount);
|
|
4
|
+
const times = [];
|
|
5
|
+
for (let i = 0; i < iterations; i++) {
|
|
6
|
+
const dirtyResults = Array.from({ length: dirtyCount }, (_, idx) => ({
|
|
7
|
+
index: idx,
|
|
8
|
+
oldHeight: undefined,
|
|
9
|
+
newHeight: 40 + Math.random() * 20
|
|
10
|
+
}));
|
|
11
|
+
const start = performance.now();
|
|
12
|
+
manager.processDirtyHeights(dirtyResults);
|
|
13
|
+
const end = performance.now();
|
|
14
|
+
times.push(end - start);
|
|
15
|
+
manager.reset(); // Reset for next iteration
|
|
16
|
+
}
|
|
17
|
+
const totalTime = times.reduce((sum, time) => sum + time, 0);
|
|
18
|
+
const avgTime = totalTime / iterations;
|
|
19
|
+
const opsPerSecond = 1000 / avgTime; // Convert ms to ops/second
|
|
20
|
+
return {
|
|
21
|
+
avgTime,
|
|
22
|
+
totalTime,
|
|
23
|
+
opsPerSecond
|
|
24
|
+
};
|
|
25
|
+
}
|