lazy-render-virtual-scroll 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/react/index.d.ts +54 -0
- package/dist/cjs/adapters/react/adapters/react/index.d.ts +3 -0
- package/dist/cjs/adapters/react/adapters/react/index.d.ts.map +1 -0
- package/dist/cjs/adapters/react/index.js +1521 -0
- package/dist/cjs/adapters/react/index.js.map +1 -0
- package/dist/cjs/angular/adapters/react/index.d.ts +3 -0
- package/dist/cjs/angular/adapters/react/index.d.ts.map +1 -0
- package/dist/cjs/svelte/adapters/react/index.d.ts +3 -0
- package/dist/cjs/svelte/adapters/react/index.d.ts.map +1 -0
- package/dist/cjs/vanilla/adapters/react/index.d.ts +3 -0
- package/dist/cjs/vanilla/adapters/react/index.d.ts.map +1 -0
- package/dist/cjs/vue/adapters/react/index.d.ts +3 -0
- package/dist/cjs/vue/adapters/react/index.d.ts.map +1 -0
- package/dist/esm/adapters/react/adapters/react/index.d.ts +3 -0
- package/dist/esm/adapters/react/adapters/react/index.d.ts.map +1 -0
- package/dist/esm/adapters/react/index.js +1518 -0
- package/dist/esm/adapters/react/index.js.map +1 -0
- package/dist/esm/angular/adapters/react/index.d.ts +3 -0
- package/dist/esm/angular/adapters/react/index.d.ts.map +1 -0
- package/dist/esm/svelte/adapters/react/index.d.ts +3 -0
- package/dist/esm/svelte/adapters/react/index.d.ts.map +1 -0
- package/dist/esm/vanilla/adapters/react/index.d.ts +3 -0
- package/dist/esm/vanilla/adapters/react/index.d.ts.map +1 -0
- package/dist/esm/vue/adapters/react/index.d.ts +3 -0
- package/dist/esm/vue/adapters/react/index.d.ts.map +1 -0
- package/package.json +6 -1
|
@@ -0,0 +1,1521 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React = require('react');
|
|
4
|
+
|
|
5
|
+
class WindowManager {
|
|
6
|
+
constructor(itemHeight, viewportHeight, bufferSize = 5) {
|
|
7
|
+
this.itemHeight = itemHeight;
|
|
8
|
+
this.viewportHeight = viewportHeight;
|
|
9
|
+
this.bufferSize = bufferSize;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Calculate the visible range based on scroll position
|
|
13
|
+
*/
|
|
14
|
+
calculateVisibleRange(scrollTop) {
|
|
15
|
+
// Calculate how many items fit in the viewport
|
|
16
|
+
const itemsPerViewport = Math.ceil(this.viewportHeight / this.itemHeight);
|
|
17
|
+
// Calculate the starting index based on scroll position
|
|
18
|
+
const startIndex = Math.floor(scrollTop / this.itemHeight);
|
|
19
|
+
// Calculate the ending index with buffer
|
|
20
|
+
const endIndex = Math.min(startIndex + itemsPerViewport + this.bufferSize, Number.MAX_SAFE_INTEGER // Will be limited by total items later
|
|
21
|
+
);
|
|
22
|
+
return {
|
|
23
|
+
start: Math.max(0, startIndex - this.bufferSize),
|
|
24
|
+
end: endIndex
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Update viewport height if it changes
|
|
29
|
+
*/
|
|
30
|
+
updateViewportHeight(height) {
|
|
31
|
+
this.viewportHeight = height;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Update item height if it changes
|
|
35
|
+
*/
|
|
36
|
+
updateItemHeight(height) {
|
|
37
|
+
this.itemHeight = height;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Update buffer size if it changes
|
|
41
|
+
*/
|
|
42
|
+
updateBufferSize(size) {
|
|
43
|
+
this.bufferSize = size;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class PrefetchManager {
|
|
48
|
+
/**
|
|
49
|
+
* This class is kept for backward compatibility
|
|
50
|
+
* Intelligent prefetching is now handled in the Engine class
|
|
51
|
+
*/
|
|
52
|
+
constructor() { }
|
|
53
|
+
/**
|
|
54
|
+
* Legacy method - not used in intelligent mode
|
|
55
|
+
*/
|
|
56
|
+
shouldPrefetch(visibleEnd, totalLoaded) {
|
|
57
|
+
// Simple rule: if visible end is approaching the loaded boundary, fetch more
|
|
58
|
+
return visibleEnd >= totalLoaded - 5; // Default buffer
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Update buffer size if it changes (for backward compatibility)
|
|
62
|
+
*/
|
|
63
|
+
updateBufferSize(size) {
|
|
64
|
+
// This method exists for backward compatibility
|
|
65
|
+
// Intelligent prefetching is now handled in the Engine class
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class RequestQueue {
|
|
70
|
+
constructor(maxConcurrent = 1) {
|
|
71
|
+
this.queue = [];
|
|
72
|
+
this.processing = false;
|
|
73
|
+
this.maxConcurrent = maxConcurrent;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Add a request to the queue
|
|
77
|
+
*/
|
|
78
|
+
add(requestFn) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
this.queue.push(() => requestFn().then(resolve).catch(reject));
|
|
81
|
+
// Start processing if not already processing
|
|
82
|
+
if (!this.processing) {
|
|
83
|
+
this.processQueue();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Process the queue
|
|
89
|
+
*/
|
|
90
|
+
async processQueue() {
|
|
91
|
+
if (this.queue.length === 0) {
|
|
92
|
+
this.processing = false;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.processing = true;
|
|
96
|
+
// Process up to maxConcurrent requests
|
|
97
|
+
const concurrentRequests = [];
|
|
98
|
+
const count = Math.min(this.maxConcurrent, this.queue.length);
|
|
99
|
+
for (let i = 0; i < count; i++) {
|
|
100
|
+
const requestFn = this.queue.shift();
|
|
101
|
+
if (requestFn) {
|
|
102
|
+
concurrentRequests.push(requestFn());
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
await Promise.all(concurrentRequests);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
console.error('Request queue error:', error);
|
|
110
|
+
}
|
|
111
|
+
// Process remaining items
|
|
112
|
+
await this.processQueue();
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Clear the queue
|
|
116
|
+
*/
|
|
117
|
+
clear() {
|
|
118
|
+
this.queue = [];
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Get the current queue length
|
|
122
|
+
*/
|
|
123
|
+
getLength() {
|
|
124
|
+
return this.queue.length;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
class IntelligentScrollDetector {
|
|
129
|
+
constructor() {
|
|
130
|
+
this.lastScrollTop = 0;
|
|
131
|
+
this.lastTime = 0;
|
|
132
|
+
this.velocityHistory = [];
|
|
133
|
+
this.HISTORY_SIZE = 5;
|
|
134
|
+
this.scrollTimeout = null;
|
|
135
|
+
this.isIdle = true;
|
|
136
|
+
this.lastTime = performance.now();
|
|
137
|
+
}
|
|
138
|
+
// Calculate velocity from scroll event
|
|
139
|
+
calculateVelocity(scrollTop) {
|
|
140
|
+
const now = performance.now();
|
|
141
|
+
const deltaY = scrollTop - this.lastScrollTop;
|
|
142
|
+
const deltaTime = now - this.lastTime;
|
|
143
|
+
// Calculate velocity (pixels per millisecond)
|
|
144
|
+
const velocity = deltaTime > 0 ? deltaY / deltaTime : 0;
|
|
145
|
+
// Store in history for smoothing
|
|
146
|
+
this.velocityHistory.push(velocity);
|
|
147
|
+
if (this.velocityHistory.length > this.HISTORY_SIZE) {
|
|
148
|
+
this.velocityHistory.shift();
|
|
149
|
+
}
|
|
150
|
+
// Update for next calculation
|
|
151
|
+
this.lastScrollTop = scrollTop;
|
|
152
|
+
this.lastTime = now;
|
|
153
|
+
// Update idle state
|
|
154
|
+
this.isIdle = false;
|
|
155
|
+
this.resetIdleTimer();
|
|
156
|
+
// Return smoothed velocity (average of recent values)
|
|
157
|
+
return this.getAverageVelocity();
|
|
158
|
+
}
|
|
159
|
+
// Get smoothed velocity from history
|
|
160
|
+
getAverageVelocity() {
|
|
161
|
+
if (this.velocityHistory.length === 0)
|
|
162
|
+
return 0;
|
|
163
|
+
const sum = this.velocityHistory.reduce((acc, vel) => acc + vel, 0);
|
|
164
|
+
return sum / this.velocityHistory.length;
|
|
165
|
+
}
|
|
166
|
+
// Determine scroll direction from velocity
|
|
167
|
+
getDirection(velocity) {
|
|
168
|
+
if (Math.abs(velocity) < 0.1)
|
|
169
|
+
return 'stationary';
|
|
170
|
+
return velocity > 0 ? 'down' : 'up';
|
|
171
|
+
}
|
|
172
|
+
// Calculate buffer size based on scroll velocity
|
|
173
|
+
calculateBuffer(velocity) {
|
|
174
|
+
const absVelocity = Math.abs(velocity);
|
|
175
|
+
if (absVelocity > 1.5) {
|
|
176
|
+
return 20; // Large buffer for fast scrolling
|
|
177
|
+
}
|
|
178
|
+
else if (absVelocity > 1.0) {
|
|
179
|
+
return 10; // Medium buffer
|
|
180
|
+
}
|
|
181
|
+
else if (absVelocity > 0.3) {
|
|
182
|
+
return 7; // Small buffer for medium scrolling
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
return 5; // Minimal buffer when nearly stationary
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Calculate prefetch distance based on velocity
|
|
189
|
+
calculatePrefetchDistance(velocity) {
|
|
190
|
+
const absVelocity = Math.abs(velocity);
|
|
191
|
+
if (absVelocity > 2.0)
|
|
192
|
+
return 1200; // Far ahead for fast scrolling
|
|
193
|
+
if (absVelocity > 1.0)
|
|
194
|
+
return 800; // Medium distance
|
|
195
|
+
if (absVelocity > 0.3)
|
|
196
|
+
return 400; // Close distance for slow scroll
|
|
197
|
+
return 200; // Minimal prefetch when nearly stationary
|
|
198
|
+
}
|
|
199
|
+
// Predict where user will be in X milliseconds
|
|
200
|
+
predictPosition(currentPosition, velocity, msAhead = 500) {
|
|
201
|
+
return currentPosition + (velocity * msAhead);
|
|
202
|
+
}
|
|
203
|
+
// Check if user is currently idle
|
|
204
|
+
getIsIdle() {
|
|
205
|
+
return this.isIdle;
|
|
206
|
+
}
|
|
207
|
+
// Reset idle timer
|
|
208
|
+
resetIdleTimer() {
|
|
209
|
+
if (this.scrollTimeout) {
|
|
210
|
+
clearTimeout(this.scrollTimeout);
|
|
211
|
+
}
|
|
212
|
+
this.scrollTimeout = window.setTimeout(() => {
|
|
213
|
+
this.isIdle = true;
|
|
214
|
+
}, 150); // 150ms after last scroll = idle
|
|
215
|
+
}
|
|
216
|
+
// Clean up resources
|
|
217
|
+
cleanup() {
|
|
218
|
+
if (this.scrollTimeout) {
|
|
219
|
+
clearTimeout(this.scrollTimeout);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
class NetworkSpeedDetector {
|
|
225
|
+
constructor() {
|
|
226
|
+
this.bandwidthHistory = [];
|
|
227
|
+
this.latencyHistory = [];
|
|
228
|
+
this.HISTORY_SIZE = 5;
|
|
229
|
+
}
|
|
230
|
+
// Estimate available bandwidth
|
|
231
|
+
async estimateBandwidth() {
|
|
232
|
+
const startTime = performance.now();
|
|
233
|
+
const testData = new Array(10000).fill('test_data').join('');
|
|
234
|
+
try {
|
|
235
|
+
// Send test request to measure bandwidth
|
|
236
|
+
const response = await fetch('/api/network-test', {
|
|
237
|
+
method: 'POST',
|
|
238
|
+
body: testData
|
|
239
|
+
});
|
|
240
|
+
const endTime = performance.now();
|
|
241
|
+
const duration = (endTime - startTime) / 1000; // seconds
|
|
242
|
+
const dataSize = testData.length; // bytes
|
|
243
|
+
const bandwidth = dataSize / duration; // bytes per second
|
|
244
|
+
this.bandwidthHistory.push(bandwidth);
|
|
245
|
+
if (this.bandwidthHistory.length > this.HISTORY_SIZE) {
|
|
246
|
+
this.bandwidthHistory.shift();
|
|
247
|
+
}
|
|
248
|
+
return this.getAverageBandwidth();
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
// If network test fails, return a conservative estimate
|
|
252
|
+
return 100000; // 100 KB/s as fallback
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Measure network latency
|
|
256
|
+
async measureLatency() {
|
|
257
|
+
try {
|
|
258
|
+
const startTime = performance.now();
|
|
259
|
+
await fetch('/api/ping');
|
|
260
|
+
const endTime = performance.now();
|
|
261
|
+
const latency = endTime - startTime;
|
|
262
|
+
this.latencyHistory.push(latency);
|
|
263
|
+
if (this.latencyHistory.length > this.HISTORY_SIZE) {
|
|
264
|
+
this.latencyHistory.shift();
|
|
265
|
+
}
|
|
266
|
+
return this.getAverageLatency();
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
// If ping fails, return a high latency as fallback
|
|
270
|
+
return 1000; // 1 second as fallback
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Assess overall connection quality
|
|
274
|
+
async assessConnectionQuality() {
|
|
275
|
+
try {
|
|
276
|
+
const [bandwidth, latency] = await Promise.all([
|
|
277
|
+
this.estimateBandwidth(),
|
|
278
|
+
this.measureLatency()
|
|
279
|
+
]);
|
|
280
|
+
if (latency > 1000)
|
|
281
|
+
return 'poor'; // High latency
|
|
282
|
+
if (bandwidth < 100000)
|
|
283
|
+
return 'poor'; // Low bandwidth (< 100 KB/s)
|
|
284
|
+
if (latency > 500 || bandwidth < 500000)
|
|
285
|
+
return 'good'; // Moderate
|
|
286
|
+
return 'excellent'; // Fast and responsive
|
|
287
|
+
}
|
|
288
|
+
catch (_a) {
|
|
289
|
+
return 'offline';
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
getAverageBandwidth() {
|
|
293
|
+
if (this.bandwidthHistory.length === 0)
|
|
294
|
+
return 0;
|
|
295
|
+
const sum = this.bandwidthHistory.reduce((a, b) => a + b, 0);
|
|
296
|
+
return sum / this.bandwidthHistory.length;
|
|
297
|
+
}
|
|
298
|
+
getAverageLatency() {
|
|
299
|
+
if (this.latencyHistory.length === 0)
|
|
300
|
+
return 0;
|
|
301
|
+
const sum = this.latencyHistory.reduce((a, b) => a + b, 0);
|
|
302
|
+
return sum / this.latencyHistory.length;
|
|
303
|
+
}
|
|
304
|
+
// Get current network statistics
|
|
305
|
+
getNetworkStats() {
|
|
306
|
+
return {
|
|
307
|
+
bandwidth: this.getAverageBandwidth(),
|
|
308
|
+
latency: this.getAverageLatency(),
|
|
309
|
+
history: [...this.bandwidthHistory]
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
class NetworkAwarePrefetchManager {
|
|
315
|
+
constructor(networkDetector) {
|
|
316
|
+
this.basePrefetchDistance = 400; // Base prefetch distance in pixels
|
|
317
|
+
this.networkDetector = networkDetector;
|
|
318
|
+
}
|
|
319
|
+
// Calculate prefetch distance based on network conditions
|
|
320
|
+
async calculateNetworkAdjustedPrefetch(velocity) {
|
|
321
|
+
const connectionQuality = await this.networkDetector.assessConnectionQuality();
|
|
322
|
+
// Base prefetch distance from scroll velocity
|
|
323
|
+
let baseDistance = this.basePrefetchDistance;
|
|
324
|
+
if (Math.abs(velocity) > 2.0)
|
|
325
|
+
baseDistance = 1200;
|
|
326
|
+
else if (Math.abs(velocity) > 1.0)
|
|
327
|
+
baseDistance = 800;
|
|
328
|
+
else if (Math.abs(velocity) > 0.3)
|
|
329
|
+
baseDistance = 400;
|
|
330
|
+
else
|
|
331
|
+
baseDistance = 200;
|
|
332
|
+
// Adjust based on network quality
|
|
333
|
+
switch (connectionQuality) {
|
|
334
|
+
case 'excellent':
|
|
335
|
+
return Math.round(baseDistance * 1.5); // Extra prefetch on fast networks
|
|
336
|
+
case 'good':
|
|
337
|
+
return Math.round(baseDistance * 1.2); // Slightly more prefetch
|
|
338
|
+
case 'poor':
|
|
339
|
+
return Math.round(baseDistance * 0.7); // Less prefetch on slow networks
|
|
340
|
+
case 'offline':
|
|
341
|
+
return Math.round(baseDistance * 0.3); // Minimal prefetch when offline
|
|
342
|
+
default:
|
|
343
|
+
return baseDistance;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Calculate batch size based on network conditions
|
|
347
|
+
async calculateNetworkAdjustedBatchSize(velocity) {
|
|
348
|
+
const connectionQuality = await this.networkDetector.assessConnectionQuality();
|
|
349
|
+
// Base batch size from scroll velocity
|
|
350
|
+
let baseBatchSize = 10; // Default batch size
|
|
351
|
+
if (Math.abs(velocity) > 2.0)
|
|
352
|
+
baseBatchSize = 20; // Fast scroll needs more
|
|
353
|
+
else if (Math.abs(velocity) > 1.0)
|
|
354
|
+
baseBatchSize = 15;
|
|
355
|
+
else if (Math.abs(velocity) > 0.3)
|
|
356
|
+
baseBatchSize = 10;
|
|
357
|
+
else
|
|
358
|
+
baseBatchSize = 5; // Slow scroll needs less
|
|
359
|
+
// Adjust based on network quality
|
|
360
|
+
switch (connectionQuality) {
|
|
361
|
+
case 'excellent':
|
|
362
|
+
return Math.min(baseBatchSize * 2, 50); // Large batches on fast networks
|
|
363
|
+
case 'good':
|
|
364
|
+
return Math.min(baseBatchSize * 1.5, 30); // Medium batches
|
|
365
|
+
case 'poor':
|
|
366
|
+
return Math.max(Math.round(baseBatchSize * 0.5), 5); // Small batches on slow networks
|
|
367
|
+
case 'offline':
|
|
368
|
+
return Math.max(Math.round(baseBatchSize * 0.3), 3); // Minimal batches when offline
|
|
369
|
+
default:
|
|
370
|
+
return baseBatchSize;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// Determine if prefetch should be delayed based on network conditions
|
|
374
|
+
async shouldDelayPrefetch() {
|
|
375
|
+
const connectionQuality = await this.networkDetector.assessConnectionQuality();
|
|
376
|
+
return connectionQuality === 'poor';
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
class NetworkAwareRequestQueue {
|
|
381
|
+
constructor(networkDetector) {
|
|
382
|
+
this.queue = [];
|
|
383
|
+
this.processing = false;
|
|
384
|
+
this.maxConcurrent = 1;
|
|
385
|
+
this.offlineQueue = [];
|
|
386
|
+
this.networkDetector = networkDetector;
|
|
387
|
+
}
|
|
388
|
+
// Add request with network-aware concurrency
|
|
389
|
+
async add(requestFn) {
|
|
390
|
+
// Adjust concurrency based on network conditions
|
|
391
|
+
const connectionQuality = await this.networkDetector.assessConnectionQuality();
|
|
392
|
+
switch (connectionQuality) {
|
|
393
|
+
case 'excellent':
|
|
394
|
+
this.maxConcurrent = 3; // Allow more concurrent requests
|
|
395
|
+
break;
|
|
396
|
+
case 'good':
|
|
397
|
+
this.maxConcurrent = 2; // Moderate concurrency
|
|
398
|
+
break;
|
|
399
|
+
case 'poor':
|
|
400
|
+
this.maxConcurrent = 1; // Sequential requests on slow networks
|
|
401
|
+
break;
|
|
402
|
+
case 'offline':
|
|
403
|
+
// Queue for later when online
|
|
404
|
+
return this.handleOfflineRequest(requestFn);
|
|
405
|
+
default:
|
|
406
|
+
this.maxConcurrent = 1;
|
|
407
|
+
}
|
|
408
|
+
return new Promise((resolve, reject) => {
|
|
409
|
+
this.queue.push(() => requestFn().then(resolve).catch(reject));
|
|
410
|
+
if (!this.processing) {
|
|
411
|
+
this.processQueue();
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
// Process queue with network-aware concurrency
|
|
416
|
+
async processQueue() {
|
|
417
|
+
if (this.queue.length === 0) {
|
|
418
|
+
this.processing = false;
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
this.processing = true;
|
|
422
|
+
// Process up to maxConcurrent requests
|
|
423
|
+
const concurrentRequests = [];
|
|
424
|
+
const count = Math.min(this.maxConcurrent, this.queue.length);
|
|
425
|
+
for (let i = 0; i < count; i++) {
|
|
426
|
+
const requestFn = this.queue.shift();
|
|
427
|
+
if (requestFn) {
|
|
428
|
+
concurrentRequests.push(requestFn());
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
await Promise.all(concurrentRequests);
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
console.error('Network-aware request queue error:', error);
|
|
436
|
+
}
|
|
437
|
+
// Process remaining items
|
|
438
|
+
await this.processQueue();
|
|
439
|
+
}
|
|
440
|
+
// Handle requests when offline
|
|
441
|
+
async handleOfflineRequest(requestFn) {
|
|
442
|
+
// Store request for later execution
|
|
443
|
+
return new Promise((resolve, reject) => {
|
|
444
|
+
// Add to offline queue
|
|
445
|
+
this.offlineQueue.push(() => requestFn().then(resolve).catch(reject));
|
|
446
|
+
// Check for network restoration periodically
|
|
447
|
+
const checkOnline = () => {
|
|
448
|
+
if (navigator.onLine) {
|
|
449
|
+
// Process offline queue
|
|
450
|
+
this.processOfflineQueue();
|
|
451
|
+
resolve(null); // Resolve with null since we can't return the actual result
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
setTimeout(checkOnline, 5000); // Check again in 5 seconds
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
checkOnline();
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
// Process offline queue when back online
|
|
461
|
+
async processOfflineQueue() {
|
|
462
|
+
const offlineRequests = [...this.offlineQueue];
|
|
463
|
+
this.offlineQueue = [];
|
|
464
|
+
for (const requestFn of offlineRequests) {
|
|
465
|
+
try {
|
|
466
|
+
await requestFn();
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
console.error('Offline request failed:', error);
|
|
470
|
+
// Add back to offline queue for retry
|
|
471
|
+
this.offlineQueue.push(requestFn);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// Get current queue status
|
|
476
|
+
getQueueStatus() {
|
|
477
|
+
return {
|
|
478
|
+
pending: this.queue.length,
|
|
479
|
+
offline: this.offlineQueue.length,
|
|
480
|
+
maxConcurrent: this.maxConcurrent
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
// Clear all queues
|
|
484
|
+
clear() {
|
|
485
|
+
this.queue = [];
|
|
486
|
+
this.offlineQueue = [];
|
|
487
|
+
this.processing = false;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
class DevicePerformanceMonitor {
|
|
492
|
+
constructor() {
|
|
493
|
+
this.frameRateHistory = [];
|
|
494
|
+
this.memoryUsageHistory = [];
|
|
495
|
+
this.gcMonitoring = false;
|
|
496
|
+
this.HISTORY_SIZE = 10;
|
|
497
|
+
this.setupPerformanceMonitoring();
|
|
498
|
+
}
|
|
499
|
+
// Monitor frame rate
|
|
500
|
+
async getFrameRate() {
|
|
501
|
+
return new Promise(resolve => {
|
|
502
|
+
const start = performance.now();
|
|
503
|
+
let frames = 0;
|
|
504
|
+
const measure = () => {
|
|
505
|
+
frames++;
|
|
506
|
+
if (frames >= 60) { // Measure over 60 frames
|
|
507
|
+
const elapsed = performance.now() - start;
|
|
508
|
+
const fps = Math.round((frames / elapsed) * 1000);
|
|
509
|
+
this.frameRateHistory.push(fps);
|
|
510
|
+
if (this.frameRateHistory.length > this.HISTORY_SIZE) {
|
|
511
|
+
this.frameRateHistory.shift();
|
|
512
|
+
}
|
|
513
|
+
resolve(fps);
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
requestAnimationFrame(measure);
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
requestAnimationFrame(measure);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
// Get average frame rate
|
|
523
|
+
getAverageFrameRate() {
|
|
524
|
+
if (this.frameRateHistory.length === 0)
|
|
525
|
+
return 60;
|
|
526
|
+
const sum = this.frameRateHistory.reduce((a, b) => a + b, 0);
|
|
527
|
+
return sum / this.frameRateHistory.length;
|
|
528
|
+
}
|
|
529
|
+
// Monitor memory usage (where available)
|
|
530
|
+
getMemoryInfo() {
|
|
531
|
+
if ('memory' in performance) {
|
|
532
|
+
// @ts-ignore - memory property is non-standard
|
|
533
|
+
const mem = performance.memory;
|
|
534
|
+
if (mem) {
|
|
535
|
+
return {
|
|
536
|
+
used: mem.usedJSHeapSize,
|
|
537
|
+
total: mem.jsHeapSizeLimit
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
// Assess overall device performance
|
|
544
|
+
async assessPerformance() {
|
|
545
|
+
const frameRate = await this.getFrameRate();
|
|
546
|
+
const memoryInfo = this.getMemoryInfo();
|
|
547
|
+
// Normalize frame rate (60fps = excellent, 30fps = poor)
|
|
548
|
+
const frameRateScore = Math.min(frameRate / 60, 1);
|
|
549
|
+
// If we have memory info, factor it in
|
|
550
|
+
if (memoryInfo) {
|
|
551
|
+
const memoryScore = 1 - (memoryInfo.used / memoryInfo.total);
|
|
552
|
+
return (frameRateScore * 0.7) + (memoryScore * 0.3);
|
|
553
|
+
}
|
|
554
|
+
return frameRateScore;
|
|
555
|
+
}
|
|
556
|
+
setupPerformanceMonitoring() {
|
|
557
|
+
// Set up performance monitoring intervals
|
|
558
|
+
setInterval(() => {
|
|
559
|
+
this.getFrameRate(); // Update frame rate history
|
|
560
|
+
}, 5000); // Every 5 seconds
|
|
561
|
+
}
|
|
562
|
+
// Get performance insights
|
|
563
|
+
getPerformanceInsights() {
|
|
564
|
+
const frameRate = this.getAverageFrameRate();
|
|
565
|
+
const memoryInfo = this.getMemoryInfo();
|
|
566
|
+
// Calculate performance score based on frame rate
|
|
567
|
+
const performanceScore = Math.min(frameRate / 60, 1);
|
|
568
|
+
return {
|
|
569
|
+
frameRate,
|
|
570
|
+
performanceScore,
|
|
571
|
+
memoryUsed: (memoryInfo === null || memoryInfo === void 0 ? void 0 : memoryInfo.used) || null,
|
|
572
|
+
memoryTotal: (memoryInfo === null || memoryInfo === void 0 ? void 0 : memoryInfo.total) || null
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
class ContentComplexityAnalyzer {
|
|
578
|
+
// Analyze content complexity based on various factors
|
|
579
|
+
analyzeContentComplexity(items) {
|
|
580
|
+
if (items.length === 0)
|
|
581
|
+
return 0.1; // Minimal complexity for empty
|
|
582
|
+
let totalComplexity = 0;
|
|
583
|
+
for (const item of items) {
|
|
584
|
+
// Analyze different aspects of complexity
|
|
585
|
+
const textComplexity = this.analyzeTextComplexity(item);
|
|
586
|
+
const mediaComplexity = this.analyzeMediaComplexity(item);
|
|
587
|
+
const componentComplexity = this.analyzeComponentComplexity(item);
|
|
588
|
+
totalComplexity += (textComplexity + mediaComplexity + componentComplexity) / 3;
|
|
589
|
+
}
|
|
590
|
+
// Return average complexity normalized to 0-1 scale
|
|
591
|
+
return Math.min(totalComplexity / items.length, 1);
|
|
592
|
+
}
|
|
593
|
+
analyzeTextComplexity(item) {
|
|
594
|
+
let complexity = 0;
|
|
595
|
+
// Length of text content
|
|
596
|
+
if (typeof item.text === 'string') {
|
|
597
|
+
complexity += Math.min(item.text.length / 1000, 0.5); // Max 0.5 for text
|
|
598
|
+
}
|
|
599
|
+
// Number of text elements
|
|
600
|
+
if (Array.isArray(item.textElements)) {
|
|
601
|
+
complexity += Math.min(item.textElements.length / 10, 0.3); // Max 0.3 for elements
|
|
602
|
+
}
|
|
603
|
+
// Formatting complexity
|
|
604
|
+
if (item.hasRichText)
|
|
605
|
+
complexity += 0.2;
|
|
606
|
+
return Math.min(complexity, 1);
|
|
607
|
+
}
|
|
608
|
+
analyzeMediaComplexity(item) {
|
|
609
|
+
let complexity = 0;
|
|
610
|
+
// Number of media elements
|
|
611
|
+
if (Array.isArray(item.media)) {
|
|
612
|
+
complexity += Math.min(item.media.length * 0.2, 0.5);
|
|
613
|
+
}
|
|
614
|
+
// Media types (images, videos are more complex than icons)
|
|
615
|
+
if (item.hasVideo)
|
|
616
|
+
complexity += 0.3;
|
|
617
|
+
if (item.hasImage)
|
|
618
|
+
complexity += 0.15;
|
|
619
|
+
if (item.hasSVG)
|
|
620
|
+
complexity += 0.1;
|
|
621
|
+
return Math.min(complexity, 1);
|
|
622
|
+
}
|
|
623
|
+
analyzeComponentComplexity(item) {
|
|
624
|
+
let complexity = 0;
|
|
625
|
+
// Number of nested components
|
|
626
|
+
if (typeof item.componentDepth === 'number') {
|
|
627
|
+
complexity += Math.min(item.componentDepth * 0.1, 0.4);
|
|
628
|
+
}
|
|
629
|
+
// Interactivity
|
|
630
|
+
if (item.interactive)
|
|
631
|
+
complexity += 0.2;
|
|
632
|
+
if (item.hasAnimations)
|
|
633
|
+
complexity += 0.2;
|
|
634
|
+
if (item.hasState)
|
|
635
|
+
complexity += 0.1;
|
|
636
|
+
return Math.min(complexity, 1);
|
|
637
|
+
}
|
|
638
|
+
// Get complexity insights
|
|
639
|
+
getComplexityInsights(items) {
|
|
640
|
+
if (items.length === 0) {
|
|
641
|
+
return {
|
|
642
|
+
averageComplexity: 0.1,
|
|
643
|
+
textComplexity: 0,
|
|
644
|
+
mediaComplexity: 0,
|
|
645
|
+
componentComplexity: 0
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
let totalText = 0, totalMedia = 0, totalComponent = 0;
|
|
649
|
+
for (const item of items) {
|
|
650
|
+
totalText += this.analyzeTextComplexity(item);
|
|
651
|
+
totalMedia += this.analyzeMediaComplexity(item);
|
|
652
|
+
totalComponent += this.analyzeComponentComplexity(item);
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
averageComplexity: this.analyzeContentComplexity(items),
|
|
656
|
+
textComplexity: totalText / items.length,
|
|
657
|
+
mediaComplexity: totalMedia / items.length,
|
|
658
|
+
componentComplexity: totalComponent / items.length
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
class AdaptiveBufferCalculator {
|
|
664
|
+
constructor() {
|
|
665
|
+
this.scrollFactor = 0.3; // Weight for scroll velocity
|
|
666
|
+
this.networkFactor = 0.3; // Weight for network quality
|
|
667
|
+
this.performanceFactor = 0.2; // Weight for device performance
|
|
668
|
+
this.contentFactor = 0.2; // Weight for content complexity
|
|
669
|
+
this.performanceMonitor = new DevicePerformanceMonitor();
|
|
670
|
+
this.contentAnalyzer = new ContentComplexityAnalyzer();
|
|
671
|
+
}
|
|
672
|
+
// Calculate optimal buffer size based on multiple factors
|
|
673
|
+
async calculateOptimalBuffer(params) {
|
|
674
|
+
// Calculate scroll-based buffer
|
|
675
|
+
const scrollBuffer = this.calculateScrollBuffer(params.scrollVelocity, params.baseBuffer);
|
|
676
|
+
// Calculate network-based adjustment
|
|
677
|
+
const networkAdjustment = this.calculateNetworkAdjustment(params.networkQuality);
|
|
678
|
+
// Calculate performance-based adjustment
|
|
679
|
+
const performanceScore = await this.performanceMonitor.assessPerformance();
|
|
680
|
+
const performanceAdjustment = this.calculatePerformanceAdjustment(performanceScore);
|
|
681
|
+
// Calculate content-based adjustment
|
|
682
|
+
const contentComplexity = this.contentAnalyzer.analyzeContentComplexity(params.visibleItems);
|
|
683
|
+
const contentAdjustment = this.calculateContentAdjustment(contentComplexity);
|
|
684
|
+
// Combine all factors
|
|
685
|
+
const weightedBuffer = (scrollBuffer * this.scrollFactor +
|
|
686
|
+
(params.baseBuffer * networkAdjustment) * this.networkFactor +
|
|
687
|
+
(params.baseBuffer * performanceAdjustment) * this.performanceFactor +
|
|
688
|
+
(params.baseBuffer * contentAdjustment) * this.contentFactor);
|
|
689
|
+
// Apply reasonable bounds
|
|
690
|
+
return Math.max(3, Math.min(50, Math.round(weightedBuffer)));
|
|
691
|
+
}
|
|
692
|
+
calculateScrollBuffer(velocity, baseBuffer) {
|
|
693
|
+
const absVelocity = Math.abs(velocity);
|
|
694
|
+
if (absVelocity > 2.0)
|
|
695
|
+
return baseBuffer * 4; // Very fast scroll
|
|
696
|
+
if (absVelocity > 1.0)
|
|
697
|
+
return baseBuffer * 2.5; // Fast scroll
|
|
698
|
+
if (absVelocity > 0.3)
|
|
699
|
+
return baseBuffer * 1.5; // Medium scroll
|
|
700
|
+
return baseBuffer * 0.8; // Slow scroll
|
|
701
|
+
}
|
|
702
|
+
calculateNetworkAdjustment(quality) {
|
|
703
|
+
switch (quality) {
|
|
704
|
+
case 'excellent': return 1.5; // More buffer on fast networks
|
|
705
|
+
case 'good': return 1.2; // Slightly more
|
|
706
|
+
case 'poor': return 0.7; // Less buffer on slow networks
|
|
707
|
+
case 'offline': return 0.5; // Minimal buffer when offline
|
|
708
|
+
default: return 1.0;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
calculatePerformanceAdjustment(performance) {
|
|
712
|
+
// performance is 0-1 scale (0 = poor, 1 = excellent)
|
|
713
|
+
return 0.5 + (performance * 0.8); // Range from 0.5 to 1.3
|
|
714
|
+
}
|
|
715
|
+
calculateContentAdjustment(complexity) {
|
|
716
|
+
// complexity is 0-1 scale (0 = simple, 1 = complex)
|
|
717
|
+
return 1.5 - (complexity * 0.8); // Range from 0.7 to 1.5
|
|
718
|
+
}
|
|
719
|
+
// Get adaptive insights
|
|
720
|
+
async getAdaptiveInsights(params) {
|
|
721
|
+
const buffer = await this.calculateOptimalBuffer(params);
|
|
722
|
+
const perfInsights = this.performanceMonitor.getPerformanceInsights();
|
|
723
|
+
const complexityInsights = this.contentAnalyzer.getComplexityInsights(params.visibleItems);
|
|
724
|
+
return {
|
|
725
|
+
currentBuffer: buffer,
|
|
726
|
+
performance: {
|
|
727
|
+
frameRate: perfInsights.frameRate,
|
|
728
|
+
score: perfInsights.performanceScore
|
|
729
|
+
},
|
|
730
|
+
network: {
|
|
731
|
+
quality: params.networkQuality,
|
|
732
|
+
adjustment: this.calculateNetworkAdjustment(params.networkQuality)
|
|
733
|
+
},
|
|
734
|
+
complexity: {
|
|
735
|
+
score: this.contentAnalyzer.analyzeContentComplexity(params.visibleItems),
|
|
736
|
+
breakdown: complexityInsights
|
|
737
|
+
},
|
|
738
|
+
factors: {
|
|
739
|
+
scroll: this.calculateScrollBuffer(params.scrollVelocity, params.baseBuffer),
|
|
740
|
+
network: this.calculateNetworkAdjustment(params.networkQuality),
|
|
741
|
+
performance: this.calculatePerformanceAdjustment(perfInsights.performanceScore),
|
|
742
|
+
content: this.calculateContentAdjustment(complexityInsights.averageComplexity)
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
class PerformanceOptimizer {
|
|
749
|
+
constructor() {
|
|
750
|
+
this.frameBudget = 16; // Target for 60fps (16.67ms per frame)
|
|
751
|
+
this.lastFrameTime = 0;
|
|
752
|
+
this.animationFrameId = null;
|
|
753
|
+
this.isOptimizing = false;
|
|
754
|
+
// Frame rate limiter to prevent excessive updates
|
|
755
|
+
this.lastUpdate = 0;
|
|
756
|
+
this.minUpdateInterval = 16; // Minimum 16ms between updates (60fps)
|
|
757
|
+
// Batch updates to reduce DOM manipulations
|
|
758
|
+
this.updateQueue = [];
|
|
759
|
+
this.isProcessingQueue = false;
|
|
760
|
+
// Memory optimization
|
|
761
|
+
this.cleanupThreshold = 1000; // Clean up items beyond this threshold
|
|
762
|
+
this.gcInterval = null;
|
|
763
|
+
this.setupPerformanceMonitoring();
|
|
764
|
+
}
|
|
765
|
+
// Optimize rendering by limiting updates to frame budget
|
|
766
|
+
scheduleOptimizedUpdate(updateFn) {
|
|
767
|
+
const now = performance.now();
|
|
768
|
+
// Throttle updates based on frame rate
|
|
769
|
+
if (now - this.lastUpdate < this.minUpdateInterval) {
|
|
770
|
+
// Queue the update for later
|
|
771
|
+
this.updateQueue.push(updateFn);
|
|
772
|
+
if (!this.isProcessingQueue) {
|
|
773
|
+
this.processUpdateQueue();
|
|
774
|
+
}
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
// Check if we have enough time in the current frame
|
|
778
|
+
if (this.getTimeRemaining() > 4) { // Leave 4ms buffer
|
|
779
|
+
updateFn();
|
|
780
|
+
this.lastUpdate = now;
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
// Schedule for next frame
|
|
784
|
+
this.updateQueue.push(updateFn);
|
|
785
|
+
if (!this.isProcessingQueue) {
|
|
786
|
+
this.processUpdateQueue();
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
// Process queued updates efficiently
|
|
791
|
+
async processUpdateQueue() {
|
|
792
|
+
if (this.updateQueue.length === 0) {
|
|
793
|
+
this.isProcessingQueue = false;
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
this.isProcessingQueue = true;
|
|
797
|
+
const currentTime = performance.now();
|
|
798
|
+
// Process as many updates as possible within frame budget
|
|
799
|
+
while (this.updateQueue.length > 0 && this.getTimeRemaining() > 2) {
|
|
800
|
+
const updateFn = this.updateQueue.shift();
|
|
801
|
+
if (updateFn) {
|
|
802
|
+
updateFn();
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
this.lastUpdate = currentTime;
|
|
806
|
+
if (this.updateQueue.length > 0) {
|
|
807
|
+
// Schedule remaining updates for next frame (only in browser environment)
|
|
808
|
+
if (typeof requestAnimationFrame !== 'undefined') {
|
|
809
|
+
requestAnimationFrame(() => this.processUpdateQueue());
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
// In Node.js environment, use setTimeout as fallback
|
|
813
|
+
setTimeout(() => this.processUpdateQueue(), 0);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
this.isProcessingQueue = false;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
// Get remaining time in current frame
|
|
821
|
+
getTimeRemaining() {
|
|
822
|
+
if (typeof performance === 'undefined' || !performance.now) {
|
|
823
|
+
return 16; // Fallback to 60fps
|
|
824
|
+
}
|
|
825
|
+
const currentTime = performance.now();
|
|
826
|
+
// Typically browsers target 10ms remaining time for smoothness
|
|
827
|
+
return Math.max(0, this.frameBudget - (currentTime - this.lastFrameTime));
|
|
828
|
+
}
|
|
829
|
+
// Memory optimization: cleanup off-screen items
|
|
830
|
+
optimizeMemory(cleanupFn, visibleRange) {
|
|
831
|
+
// Determine cleanup range (items far from visible range)
|
|
832
|
+
const cleanupStart = Math.max(0, visibleRange.end + this.cleanupThreshold);
|
|
833
|
+
const cleanupEnd = Math.max(0, visibleRange.start - this.cleanupThreshold);
|
|
834
|
+
if (cleanupStart > visibleRange.end) {
|
|
835
|
+
cleanupFn(visibleRange.end, cleanupStart);
|
|
836
|
+
}
|
|
837
|
+
if (cleanupEnd < visibleRange.start) {
|
|
838
|
+
cleanupFn(cleanupEnd, visibleRange.start);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
// Enable GPU acceleration for smoother scrolling
|
|
842
|
+
enableGPUCssAcceleration(element) {
|
|
843
|
+
// Force hardware acceleration
|
|
844
|
+
element.style.willChange = 'transform';
|
|
845
|
+
element.style.transform = 'translateZ(0)';
|
|
846
|
+
element.style.backfaceVisibility = 'hidden';
|
|
847
|
+
}
|
|
848
|
+
// Disable GPU acceleration when not needed
|
|
849
|
+
disableGPUCssAcceleration(element) {
|
|
850
|
+
element.style.willChange = 'auto';
|
|
851
|
+
element.style.transform = '';
|
|
852
|
+
element.style.backfaceVisibility = '';
|
|
853
|
+
}
|
|
854
|
+
// Optimize for different device capabilities
|
|
855
|
+
getOptimizationProfile() {
|
|
856
|
+
// Simple profile detection based on common device characteristics
|
|
857
|
+
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
|
|
858
|
+
// Detect low-end devices
|
|
859
|
+
if (this.isLowEndDevice(userAgent)) {
|
|
860
|
+
return {
|
|
861
|
+
frameRate: 30, // Lower target for low-end devices
|
|
862
|
+
batchSize: 5, // Smaller batches
|
|
863
|
+
bufferMultiplier: 0.5, // Smaller buffer
|
|
864
|
+
updateInterval: 32 // 30fps interval
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
// Default profile for capable devices
|
|
868
|
+
return {
|
|
869
|
+
frameRate: 60,
|
|
870
|
+
batchSize: 10,
|
|
871
|
+
bufferMultiplier: 1.0,
|
|
872
|
+
updateInterval: 16 // 60fps interval
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
isLowEndDevice(userAgent) {
|
|
876
|
+
// Simple heuristic for low-end devices
|
|
877
|
+
const lowEndPatterns = [
|
|
878
|
+
/Android.*Mobile/,
|
|
879
|
+
/iPhone.*OS [0-9]+_[0-9]+/,
|
|
880
|
+
/Opera Mini/,
|
|
881
|
+
/IEMobile/
|
|
882
|
+
];
|
|
883
|
+
return lowEndPatterns.some(pattern => pattern.test(userAgent));
|
|
884
|
+
}
|
|
885
|
+
// Setup performance monitoring
|
|
886
|
+
setupPerformanceMonitoring() {
|
|
887
|
+
// Check if we're in a browser environment
|
|
888
|
+
if (typeof window === 'undefined' || typeof requestAnimationFrame === 'undefined') {
|
|
889
|
+
// In Node.js environment, skip browser-specific monitoring
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
// Monitor frame rate
|
|
893
|
+
let frameCount = 0;
|
|
894
|
+
let lastTime = performance.now();
|
|
895
|
+
const monitorFrameRate = () => {
|
|
896
|
+
frameCount++;
|
|
897
|
+
const currentTime = performance.now();
|
|
898
|
+
if (currentTime - lastTime >= 1000) { // Every second
|
|
899
|
+
const fps = frameCount;
|
|
900
|
+
frameCount = 0;
|
|
901
|
+
lastTime = currentTime;
|
|
902
|
+
// Adjust optimization based on actual FPS
|
|
903
|
+
if (fps < 30) {
|
|
904
|
+
this.frameBudget = 32; // Target 30fps
|
|
905
|
+
}
|
|
906
|
+
else if (fps < 50) {
|
|
907
|
+
this.frameBudget = 20; // Target 50fps
|
|
908
|
+
}
|
|
909
|
+
else {
|
|
910
|
+
this.frameBudget = 16; // Target 60fps
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
this.animationFrameId = requestAnimationFrame(monitorFrameRate);
|
|
914
|
+
};
|
|
915
|
+
this.animationFrameId = requestAnimationFrame(monitorFrameRate);
|
|
916
|
+
// Setup garbage collection monitoring
|
|
917
|
+
this.gcInterval = window.setInterval(() => {
|
|
918
|
+
if ('gc' in window) {
|
|
919
|
+
// @ts-ignore - gc is non-standard
|
|
920
|
+
window.gc();
|
|
921
|
+
}
|
|
922
|
+
}, 30000); // GC every 30 seconds
|
|
923
|
+
}
|
|
924
|
+
// Get performance insights
|
|
925
|
+
getPerformanceInsights() {
|
|
926
|
+
return {
|
|
927
|
+
frameRate: 60, // Would be calculated from monitoring
|
|
928
|
+
memoryUsage: this.getMemoryUsage(),
|
|
929
|
+
updateFrequency: 1000 / this.minUpdateInterval,
|
|
930
|
+
optimizationActive: this.isOptimizing
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
getMemoryUsage() {
|
|
934
|
+
var _a;
|
|
935
|
+
if ('memory' in performance) {
|
|
936
|
+
// @ts-ignore - memory property is non-standard
|
|
937
|
+
return ((_a = performance.memory) === null || _a === void 0 ? void 0 : _a.usedJSHeapSize) || null;
|
|
938
|
+
}
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
// Cleanup resources
|
|
942
|
+
cleanup() {
|
|
943
|
+
if (this.animationFrameId && typeof cancelAnimationFrame !== 'undefined') {
|
|
944
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
945
|
+
}
|
|
946
|
+
if (this.gcInterval && typeof clearInterval !== 'undefined') {
|
|
947
|
+
clearInterval(this.gcInterval);
|
|
948
|
+
}
|
|
949
|
+
this.updateQueue = [];
|
|
950
|
+
this.isProcessingQueue = false;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
class MemoryManager {
|
|
955
|
+
constructor(maxCacheSize = 1000) {
|
|
956
|
+
this.itemCache = new Map();
|
|
957
|
+
this.maxCacheSize = 1000; // Maximum items to keep in cache
|
|
958
|
+
this.cleanupThreshold = 500; // Start cleanup when cache exceeds this
|
|
959
|
+
this.visibleRange = { start: 0, end: 0 };
|
|
960
|
+
this.totalItems = 0;
|
|
961
|
+
this.maxCacheSize = maxCacheSize;
|
|
962
|
+
}
|
|
963
|
+
// Set visible range to optimize cache
|
|
964
|
+
setVisibleRange(range) {
|
|
965
|
+
this.visibleRange = range;
|
|
966
|
+
}
|
|
967
|
+
// Set total number of items
|
|
968
|
+
setTotalItems(total) {
|
|
969
|
+
this.totalItems = total;
|
|
970
|
+
}
|
|
971
|
+
// Get item from cache
|
|
972
|
+
get(key) {
|
|
973
|
+
return this.itemCache.get(key) || null;
|
|
974
|
+
}
|
|
975
|
+
// Set item in cache
|
|
976
|
+
set(key, value) {
|
|
977
|
+
this.itemCache.set(key, value);
|
|
978
|
+
// Clean up if cache is too large
|
|
979
|
+
if (this.itemCache.size > this.maxCacheSize) {
|
|
980
|
+
this.cleanupCache();
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
// Check if item exists in cache
|
|
984
|
+
has(key) {
|
|
985
|
+
return this.itemCache.has(key);
|
|
986
|
+
}
|
|
987
|
+
// Remove item from cache
|
|
988
|
+
delete(key) {
|
|
989
|
+
return this.itemCache.delete(key);
|
|
990
|
+
}
|
|
991
|
+
// Clear entire cache
|
|
992
|
+
clear() {
|
|
993
|
+
this.itemCache.clear();
|
|
994
|
+
}
|
|
995
|
+
// Clean up cache based on visibility and distance from visible range
|
|
996
|
+
cleanupCache() {
|
|
997
|
+
if (this.itemCache.size <= this.cleanupThreshold) {
|
|
998
|
+
return; // No need to clean up
|
|
999
|
+
}
|
|
1000
|
+
const itemsToRemove = [];
|
|
1001
|
+
// Find items that are far from visible range
|
|
1002
|
+
for (const [key] of this.itemCache.entries()) {
|
|
1003
|
+
const distanceFromVisible = this.getDistanceFromVisible(key);
|
|
1004
|
+
// Remove items that are far from visible range
|
|
1005
|
+
if (distanceFromVisible > 100) { // Arbitrary threshold
|
|
1006
|
+
itemsToRemove.push(key);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
// If we still have too many items, remove oldest accessed items
|
|
1010
|
+
if (this.itemCache.size - itemsToRemove.length > this.cleanupThreshold) {
|
|
1011
|
+
const sortedKeys = Array.from(this.itemCache.keys())
|
|
1012
|
+
.sort((a, b) => a - b); // Sort by key (assuming they're indexes)
|
|
1013
|
+
// Remove items that are furthest from visible range
|
|
1014
|
+
for (const key of sortedKeys) {
|
|
1015
|
+
if (this.itemCache.size <= this.cleanupThreshold)
|
|
1016
|
+
break;
|
|
1017
|
+
const distance = this.getDistanceFromVisible(key);
|
|
1018
|
+
if (distance > 50) { // Remove items beyond 50 units from visible
|
|
1019
|
+
itemsToRemove.push(key);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
// Actually remove items
|
|
1024
|
+
for (const key of itemsToRemove) {
|
|
1025
|
+
this.itemCache.delete(key);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
// Calculate distance from visible range
|
|
1029
|
+
getDistanceFromVisible(index) {
|
|
1030
|
+
if (index >= this.visibleRange.start && index <= this.visibleRange.end) {
|
|
1031
|
+
return 0; // Inside visible range
|
|
1032
|
+
}
|
|
1033
|
+
if (index < this.visibleRange.start) {
|
|
1034
|
+
return this.visibleRange.start - index;
|
|
1035
|
+
}
|
|
1036
|
+
return index - this.visibleRange.end;
|
|
1037
|
+
}
|
|
1038
|
+
// Get cache statistics
|
|
1039
|
+
getStats() {
|
|
1040
|
+
const visibleItems = Array.from(this.itemCache.keys())
|
|
1041
|
+
.filter(key => key >= this.visibleRange.start && key <= this.visibleRange.end)
|
|
1042
|
+
.length;
|
|
1043
|
+
const offScreenItems = this.itemCache.size - visibleItems;
|
|
1044
|
+
// Rough estimate of memory usage (in bytes)
|
|
1045
|
+
let memoryEstimate = 0;
|
|
1046
|
+
for (const [_, value] of this.itemCache.entries()) {
|
|
1047
|
+
memoryEstimate += this.estimateObjectSize(value);
|
|
1048
|
+
}
|
|
1049
|
+
return {
|
|
1050
|
+
size: this.itemCache.size,
|
|
1051
|
+
maxCacheSize: this.maxCacheSize,
|
|
1052
|
+
visibleItems,
|
|
1053
|
+
offScreenItems,
|
|
1054
|
+
memoryEstimate
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
// Estimate object size in bytes
|
|
1058
|
+
estimateObjectSize(obj) {
|
|
1059
|
+
if (obj === null || obj === undefined)
|
|
1060
|
+
return 0;
|
|
1061
|
+
if (typeof obj === 'string')
|
|
1062
|
+
return obj.length * 2; // UTF-16 chars
|
|
1063
|
+
if (typeof obj === 'number')
|
|
1064
|
+
return 8; // 8 bytes for number
|
|
1065
|
+
if (typeof obj === 'boolean')
|
|
1066
|
+
return 4; // 4 bytes for boolean
|
|
1067
|
+
if (typeof obj === 'object') {
|
|
1068
|
+
let size = 0;
|
|
1069
|
+
for (const key in obj) {
|
|
1070
|
+
if (obj.hasOwnProperty(key)) {
|
|
1071
|
+
size += key.length * 2; // Key size
|
|
1072
|
+
size += this.estimateObjectSize(obj[key]); // Value size
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
return size;
|
|
1076
|
+
}
|
|
1077
|
+
return 0; // Other types
|
|
1078
|
+
}
|
|
1079
|
+
// Prune cache to only keep essential items
|
|
1080
|
+
pruneEssential() {
|
|
1081
|
+
const essentialItems = [];
|
|
1082
|
+
// Keep items in visible range and nearby
|
|
1083
|
+
for (const [key, value] of this.itemCache.entries()) {
|
|
1084
|
+
if (this.isEssential(key)) {
|
|
1085
|
+
essentialItems.push([key, value]);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
// Clear cache and repopulate with essential items
|
|
1089
|
+
this.itemCache.clear();
|
|
1090
|
+
for (const [key, value] of essentialItems) {
|
|
1091
|
+
this.itemCache.set(key, value);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
// Check if item is essential (within buffer zone)
|
|
1095
|
+
isEssential(index) {
|
|
1096
|
+
const bufferZone = 20; // Keep items within 20 positions of visible range
|
|
1097
|
+
return index >= (this.visibleRange.start - bufferZone) &&
|
|
1098
|
+
index <= (this.visibleRange.end + bufferZone);
|
|
1099
|
+
}
|
|
1100
|
+
// Get cache size
|
|
1101
|
+
getSize() {
|
|
1102
|
+
return this.itemCache.size;
|
|
1103
|
+
}
|
|
1104
|
+
// Get cache keys
|
|
1105
|
+
getKeys() {
|
|
1106
|
+
return Array.from(this.itemCache.keys());
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
class GPUAccelerator {
|
|
1111
|
+
constructor() {
|
|
1112
|
+
this.gpuAccelerationEnabled = false;
|
|
1113
|
+
this.gpuElements = new WeakSet();
|
|
1114
|
+
this.animationFrameId = null;
|
|
1115
|
+
this.gpuAccelerationEnabled = this.isGPUSupported();
|
|
1116
|
+
}
|
|
1117
|
+
// Check if GPU acceleration is supported
|
|
1118
|
+
isGPUSupported() {
|
|
1119
|
+
// Check if we're in a browser environment
|
|
1120
|
+
if (typeof document === 'undefined') {
|
|
1121
|
+
return false; // Not supported in Node.js environment
|
|
1122
|
+
}
|
|
1123
|
+
// Check for 3D transform support
|
|
1124
|
+
const testEl = document.createElement('div');
|
|
1125
|
+
return testEl.style.webkitTransform !== undefined ||
|
|
1126
|
+
testEl.style.transform !== undefined;
|
|
1127
|
+
}
|
|
1128
|
+
// Enable GPU acceleration for an element
|
|
1129
|
+
enableForElement(element) {
|
|
1130
|
+
if (!this.gpuAccelerationEnabled)
|
|
1131
|
+
return;
|
|
1132
|
+
// Apply GPU-accelerated styles
|
|
1133
|
+
element.style.willChange = 'transform';
|
|
1134
|
+
element.style.transform = 'translateZ(0)';
|
|
1135
|
+
element.style.backfaceVisibility = 'hidden';
|
|
1136
|
+
element.style.perspective = '1000px';
|
|
1137
|
+
// Add to tracked elements
|
|
1138
|
+
this.gpuElements.add(element);
|
|
1139
|
+
}
|
|
1140
|
+
// Disable GPU acceleration for an element
|
|
1141
|
+
disableForElement(element) {
|
|
1142
|
+
if (!this.gpuAccelerationEnabled)
|
|
1143
|
+
return;
|
|
1144
|
+
// Remove GPU-accelerated styles
|
|
1145
|
+
element.style.willChange = 'auto';
|
|
1146
|
+
element.style.transform = '';
|
|
1147
|
+
element.style.backfaceVisibility = '';
|
|
1148
|
+
element.style.perspective = '';
|
|
1149
|
+
// Remove from tracked elements
|
|
1150
|
+
this.gpuElements.delete(element);
|
|
1151
|
+
}
|
|
1152
|
+
// Apply GPU acceleration to a list of elements
|
|
1153
|
+
enableForElements(elements) {
|
|
1154
|
+
elements.forEach(el => this.enableForElement(el));
|
|
1155
|
+
}
|
|
1156
|
+
// Batch update GPU acceleration
|
|
1157
|
+
batchUpdate(elements, enable) {
|
|
1158
|
+
if (!this.gpuAccelerationEnabled)
|
|
1159
|
+
return;
|
|
1160
|
+
if (enable) {
|
|
1161
|
+
this.enableForElements(elements);
|
|
1162
|
+
}
|
|
1163
|
+
else {
|
|
1164
|
+
elements.forEach(el => this.disableForElement(el));
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
// Optimize scrolling container for GPU acceleration
|
|
1168
|
+
optimizeScrollContainer(container) {
|
|
1169
|
+
if (!this.gpuAccelerationEnabled)
|
|
1170
|
+
return;
|
|
1171
|
+
// Apply optimizations to container
|
|
1172
|
+
container.style.transform = 'translateZ(0)';
|
|
1173
|
+
container.style.willChange = 'scroll-position';
|
|
1174
|
+
container.style.webkitOverflowScrolling = 'touch'; // For iOS
|
|
1175
|
+
}
|
|
1176
|
+
// Optimize individual items for GPU acceleration
|
|
1177
|
+
optimizeItem(item) {
|
|
1178
|
+
if (!this.gpuAccelerationEnabled)
|
|
1179
|
+
return;
|
|
1180
|
+
// Apply lightweight GPU acceleration
|
|
1181
|
+
item.style.transform = 'translateZ(0)';
|
|
1182
|
+
item.style.willChange = 'transform';
|
|
1183
|
+
}
|
|
1184
|
+
// Get GPU acceleration status
|
|
1185
|
+
getStatus() {
|
|
1186
|
+
// Since WeakSet doesn't have a size property, we can't count directly
|
|
1187
|
+
// This is a limitation of WeakSet
|
|
1188
|
+
return {
|
|
1189
|
+
enabled: this.gpuAccelerationEnabled,
|
|
1190
|
+
supported: this.isGPUSupported(),
|
|
1191
|
+
elementCount: 0 // Placeholder - would need different tracking method
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
// Optimize for different scenarios
|
|
1195
|
+
optimizeForScenario(scenario) {
|
|
1196
|
+
if (!this.gpuAccelerationEnabled)
|
|
1197
|
+
return;
|
|
1198
|
+
switch (scenario) {
|
|
1199
|
+
case 'scrolling':
|
|
1200
|
+
// Optimize for smooth scrolling
|
|
1201
|
+
document.body.style.willChange = 'transform';
|
|
1202
|
+
break;
|
|
1203
|
+
case 'animation':
|
|
1204
|
+
// Optimize for animations
|
|
1205
|
+
document.body.style.transform = 'translateZ(0)';
|
|
1206
|
+
break;
|
|
1207
|
+
case 'static':
|
|
1208
|
+
// Remove optimizations when not needed
|
|
1209
|
+
document.body.style.willChange = 'auto';
|
|
1210
|
+
document.body.style.transform = '';
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
// Cleanup GPU acceleration resources
|
|
1215
|
+
cleanup() {
|
|
1216
|
+
if (this.animationFrameId) {
|
|
1217
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
1218
|
+
}
|
|
1219
|
+
// Reset any applied styles (would need to track them)
|
|
1220
|
+
this.gpuElements = new WeakSet();
|
|
1221
|
+
}
|
|
1222
|
+
// Check if element has GPU acceleration enabled
|
|
1223
|
+
isAccelerated(element) {
|
|
1224
|
+
return this.gpuElements.has(element);
|
|
1225
|
+
}
|
|
1226
|
+
// Get optimization recommendations
|
|
1227
|
+
getRecommendations() {
|
|
1228
|
+
const recommendations = [];
|
|
1229
|
+
if (!this.gpuAccelerationEnabled) {
|
|
1230
|
+
recommendations.push('GPU acceleration not supported on this device');
|
|
1231
|
+
}
|
|
1232
|
+
else {
|
|
1233
|
+
recommendations.push('GPU acceleration enabled for smooth performance');
|
|
1234
|
+
recommendations.push('Using hardware-accelerated compositing');
|
|
1235
|
+
recommendations.push('Optimized for 60fps rendering');
|
|
1236
|
+
}
|
|
1237
|
+
return recommendations;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
class Engine {
|
|
1242
|
+
constructor(config) {
|
|
1243
|
+
this.fetchMoreCallback = null;
|
|
1244
|
+
this.config = {
|
|
1245
|
+
...config,
|
|
1246
|
+
bufferSize: config.bufferSize || 5
|
|
1247
|
+
};
|
|
1248
|
+
this.windowManager = new WindowManager(this.config.itemHeight, this.config.viewportHeight, this.config.bufferSize);
|
|
1249
|
+
this.prefetchManager = new PrefetchManager(this.config.bufferSize);
|
|
1250
|
+
this.requestQueue = new RequestQueue(1); // Single request at a time
|
|
1251
|
+
this.intelligentScrollDetector = new IntelligentScrollDetector();
|
|
1252
|
+
this.networkDetector = new NetworkSpeedDetector();
|
|
1253
|
+
this.networkAwarePrefetchManager = new NetworkAwarePrefetchManager(this.networkDetector);
|
|
1254
|
+
this.networkAwareRequestQueue = new NetworkAwareRequestQueue(this.networkDetector);
|
|
1255
|
+
this.adaptiveBufferCalculator = new AdaptiveBufferCalculator();
|
|
1256
|
+
this.performanceOptimizer = new PerformanceOptimizer();
|
|
1257
|
+
this.memoryManager = new MemoryManager(1000); // Cache up to 1000 items
|
|
1258
|
+
this.gpuAccelerator = new GPUAccelerator();
|
|
1259
|
+
this.totalItems = this.config.totalItems || Number.MAX_SAFE_INTEGER;
|
|
1260
|
+
this.state = {
|
|
1261
|
+
scrollTop: 0,
|
|
1262
|
+
visibleRange: { start: 0, end: 0 },
|
|
1263
|
+
loadedItems: 0,
|
|
1264
|
+
isLoading: false
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Update scroll position and recalculate visible range with intelligent detection
|
|
1269
|
+
*/
|
|
1270
|
+
async updateScrollPosition(scrollTop) {
|
|
1271
|
+
// Use performance optimizer to schedule updates efficiently
|
|
1272
|
+
this.performanceOptimizer.scheduleOptimizedUpdate(async () => {
|
|
1273
|
+
// Calculate velocity and other intelligent metrics
|
|
1274
|
+
const velocity = this.intelligentScrollDetector.calculateVelocity(scrollTop);
|
|
1275
|
+
this.intelligentScrollDetector.getDirection(velocity);
|
|
1276
|
+
// Get network quality for adaptive buffering
|
|
1277
|
+
const networkQuality = await this.networkDetector.assessConnectionQuality();
|
|
1278
|
+
// Calculate adaptive buffer considering all factors
|
|
1279
|
+
const adaptiveBuffer = await this.adaptiveBufferCalculator.calculateOptimalBuffer({
|
|
1280
|
+
scrollVelocity: velocity,
|
|
1281
|
+
networkQuality,
|
|
1282
|
+
baseBuffer: this.intelligentScrollDetector.calculateBuffer(velocity),
|
|
1283
|
+
visibleItems: [] // In a real implementation, this would be the actual visible items
|
|
1284
|
+
});
|
|
1285
|
+
// Update window manager with adaptive buffer
|
|
1286
|
+
this.windowManager.updateBufferSize(adaptiveBuffer);
|
|
1287
|
+
// Update memory manager with visible range
|
|
1288
|
+
this.memoryManager.setVisibleRange({
|
|
1289
|
+
start: Math.max(0, this.state.visibleRange.start - adaptiveBuffer),
|
|
1290
|
+
end: this.state.visibleRange.end + adaptiveBuffer
|
|
1291
|
+
});
|
|
1292
|
+
this.state.scrollTop = scrollTop;
|
|
1293
|
+
this.state.visibleRange = this.windowManager.calculateVisibleRange(scrollTop);
|
|
1294
|
+
// Check if we need to fetch more items
|
|
1295
|
+
if (await this.shouldFetchMore()) {
|
|
1296
|
+
await this.fetchMore();
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Get the current visible range
|
|
1302
|
+
*/
|
|
1303
|
+
getVisibleRange() {
|
|
1304
|
+
return this.state.visibleRange;
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Check if more items should be fetched with intelligent and network-aware detection
|
|
1308
|
+
*/
|
|
1309
|
+
async shouldFetchMore() {
|
|
1310
|
+
if (!this.fetchMoreCallback)
|
|
1311
|
+
return false;
|
|
1312
|
+
if (this.state.isLoading)
|
|
1313
|
+
return false;
|
|
1314
|
+
if (this.state.loadedItems >= this.totalItems)
|
|
1315
|
+
return false;
|
|
1316
|
+
// Get current velocity for intelligent prefetching
|
|
1317
|
+
const velocity = this.intelligentScrollDetector.calculateVelocity(this.state.scrollTop);
|
|
1318
|
+
// Get network quality for adaptive prefetching
|
|
1319
|
+
await this.networkDetector.assessConnectionQuality();
|
|
1320
|
+
// Calculate network-adjusted prefetch distance
|
|
1321
|
+
const prefetchDistance = await this.networkAwarePrefetchManager.calculateNetworkAdjustedPrefetch(velocity);
|
|
1322
|
+
// Use intelligent prefetch logic
|
|
1323
|
+
const visibleEnd = this.state.visibleRange.end;
|
|
1324
|
+
const totalLoaded = this.state.loadedItems;
|
|
1325
|
+
// Intelligent prefetch: if visible end is approaching the loaded boundary
|
|
1326
|
+
return visibleEnd >= totalLoaded - prefetchDistance;
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Fetch more items with network awareness
|
|
1330
|
+
*/
|
|
1331
|
+
async fetchMore() {
|
|
1332
|
+
if (!this.fetchMoreCallback || this.state.isLoading)
|
|
1333
|
+
return;
|
|
1334
|
+
this.state.isLoading = true;
|
|
1335
|
+
try {
|
|
1336
|
+
// Use network-aware request queue
|
|
1337
|
+
const result = await this.networkAwareRequestQueue.add(this.fetchMoreCallback);
|
|
1338
|
+
// Assuming the result contains new items
|
|
1339
|
+
// In a real implementation, this would update the loaded items count
|
|
1340
|
+
this.state.loadedItems += Array.isArray(result) ? result.length : 1;
|
|
1341
|
+
}
|
|
1342
|
+
catch (error) {
|
|
1343
|
+
console.error('Error fetching more items:', error);
|
|
1344
|
+
}
|
|
1345
|
+
finally {
|
|
1346
|
+
this.state.isLoading = false;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Set the fetchMore callback function
|
|
1351
|
+
*/
|
|
1352
|
+
setFetchMoreCallback(callback) {
|
|
1353
|
+
this.fetchMoreCallback = callback;
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Update total items count
|
|
1357
|
+
*/
|
|
1358
|
+
updateTotalItems(count) {
|
|
1359
|
+
this.totalItems = count;
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Get current engine state
|
|
1363
|
+
*/
|
|
1364
|
+
getState() {
|
|
1365
|
+
return { ...this.state };
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Update viewport dimensions
|
|
1369
|
+
*/
|
|
1370
|
+
updateDimensions(viewportHeight, itemHeight) {
|
|
1371
|
+
this.windowManager.updateViewportHeight(viewportHeight);
|
|
1372
|
+
this.windowManager.updateItemHeight(itemHeight);
|
|
1373
|
+
// Recalculate visible range with new dimensions
|
|
1374
|
+
this.state.visibleRange = this.windowManager.calculateVisibleRange(this.state.scrollTop);
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Cleanup resources
|
|
1378
|
+
*/
|
|
1379
|
+
cleanup() {
|
|
1380
|
+
this.requestQueue.clear();
|
|
1381
|
+
this.networkAwareRequestQueue.clear();
|
|
1382
|
+
this.fetchMoreCallback = null;
|
|
1383
|
+
this.intelligentScrollDetector.cleanup();
|
|
1384
|
+
this.performanceOptimizer.cleanup();
|
|
1385
|
+
this.memoryManager.clear();
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
const useLazyList = (config) => {
|
|
1390
|
+
const { fetchMore, ...engineConfig } = config;
|
|
1391
|
+
const engineRef = React.useRef(null);
|
|
1392
|
+
const containerRef = React.useRef(null);
|
|
1393
|
+
const [visibleRange, setVisibleRange] = React.useState({ start: 0, end: 0 });
|
|
1394
|
+
const [loadedItems, setLoadedItems] = React.useState([]);
|
|
1395
|
+
const [isLoading, setIsLoading] = React.useState(false);
|
|
1396
|
+
const [scrollAnalysis, setScrollAnalysis] = React.useState({
|
|
1397
|
+
velocity: 0,
|
|
1398
|
+
direction: 'stationary',
|
|
1399
|
+
buffer: 5,
|
|
1400
|
+
prefetchDistance: 400,
|
|
1401
|
+
predictedPosition: 0,
|
|
1402
|
+
isIdle: true
|
|
1403
|
+
});
|
|
1404
|
+
// Initialize engine
|
|
1405
|
+
React.useEffect(() => {
|
|
1406
|
+
engineRef.current = new Engine(engineConfig);
|
|
1407
|
+
engineRef.current.setFetchMoreCallback(fetchMore);
|
|
1408
|
+
return () => {
|
|
1409
|
+
if (engineRef.current) {
|
|
1410
|
+
engineRef.current.cleanup();
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
}, []);
|
|
1414
|
+
// Update engine when config changes
|
|
1415
|
+
React.useEffect(() => {
|
|
1416
|
+
if (engineRef.current) {
|
|
1417
|
+
engineRef.current.updateDimensions(engineConfig.viewportHeight, engineConfig.itemHeight);
|
|
1418
|
+
}
|
|
1419
|
+
}, [engineConfig.viewportHeight, engineConfig.itemHeight]);
|
|
1420
|
+
// Handle scroll events
|
|
1421
|
+
const handleScroll = React.useCallback((scrollTop) => {
|
|
1422
|
+
if (engineRef.current) {
|
|
1423
|
+
engineRef.current.updateScrollPosition(scrollTop);
|
|
1424
|
+
// Update state based on engine
|
|
1425
|
+
const state = engineRef.current.getState();
|
|
1426
|
+
setVisibleRange(state.visibleRange);
|
|
1427
|
+
setIsLoading(state.isLoading);
|
|
1428
|
+
}
|
|
1429
|
+
}, []);
|
|
1430
|
+
// Set container reference
|
|
1431
|
+
const setContainerRef = (element) => {
|
|
1432
|
+
if (element) {
|
|
1433
|
+
containerRef.current = element;
|
|
1434
|
+
// Initialize scroll observer when container is available
|
|
1435
|
+
if (typeof window !== 'undefined' && element) {
|
|
1436
|
+
// In a real implementation, we would use ScrollObserver here
|
|
1437
|
+
// For now, we'll just attach a basic scroll listener
|
|
1438
|
+
const handleScrollEvent = () => {
|
|
1439
|
+
handleScroll(element.scrollTop);
|
|
1440
|
+
};
|
|
1441
|
+
element.addEventListener('scroll', handleScrollEvent, { passive: true });
|
|
1442
|
+
// Cleanup
|
|
1443
|
+
return () => {
|
|
1444
|
+
element.removeEventListener('scroll', handleScrollEvent);
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
return {
|
|
1450
|
+
visibleRange,
|
|
1451
|
+
loadedItems,
|
|
1452
|
+
isLoading,
|
|
1453
|
+
scrollAnalysis,
|
|
1454
|
+
setContainerRef,
|
|
1455
|
+
// Helper function to trigger manual refresh
|
|
1456
|
+
refresh: () => {
|
|
1457
|
+
var _a;
|
|
1458
|
+
if (engineRef.current) {
|
|
1459
|
+
engineRef.current.updateScrollPosition(((_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.scrollTop) || 0);
|
|
1460
|
+
}
|
|
1461
|
+
},
|
|
1462
|
+
// Function to get current scroll analysis
|
|
1463
|
+
getScrollAnalysis: () => {
|
|
1464
|
+
if (engineRef.current) {
|
|
1465
|
+
// In a real implementation, we would get the analysis from the engine
|
|
1466
|
+
// For now, we'll return the current state
|
|
1467
|
+
return scrollAnalysis;
|
|
1468
|
+
}
|
|
1469
|
+
return {
|
|
1470
|
+
velocity: 0,
|
|
1471
|
+
direction: 'stationary',
|
|
1472
|
+
buffer: 5,
|
|
1473
|
+
prefetchDistance: 400,
|
|
1474
|
+
predictedPosition: 0,
|
|
1475
|
+
isIdle: true
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
};
|
|
1480
|
+
|
|
1481
|
+
const LazyList = React.forwardRef((props, ref) => {
|
|
1482
|
+
const { fetchMore, renderItem, items, itemHeight, viewportHeight, bufferSize, className = '', style = {}, ...rest } = props;
|
|
1483
|
+
const { visibleRange, setContainerRef, isLoading } = useLazyList({
|
|
1484
|
+
fetchMore,
|
|
1485
|
+
itemHeight,
|
|
1486
|
+
viewportHeight,
|
|
1487
|
+
bufferSize,
|
|
1488
|
+
...rest
|
|
1489
|
+
});
|
|
1490
|
+
// Calculate container height to simulate infinite scroll
|
|
1491
|
+
const containerHeight = items.length * itemHeight;
|
|
1492
|
+
const visibleItems = items.slice(visibleRange.start, visibleRange.end);
|
|
1493
|
+
// Calculate top padding to maintain scroll position
|
|
1494
|
+
const paddingTop = visibleRange.start * itemHeight;
|
|
1495
|
+
return (React.createElement("div", { ref: (el) => {
|
|
1496
|
+
setContainerRef(el);
|
|
1497
|
+
if (ref) {
|
|
1498
|
+
if (typeof ref === 'function') {
|
|
1499
|
+
ref(el);
|
|
1500
|
+
}
|
|
1501
|
+
else {
|
|
1502
|
+
ref.current = el;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}, className: `lazy-list ${className}`, style: {
|
|
1506
|
+
height: `${viewportHeight}px`,
|
|
1507
|
+
overflowY: 'auto',
|
|
1508
|
+
...style
|
|
1509
|
+
}, ...rest },
|
|
1510
|
+
React.createElement("div", { style: { height: `${paddingTop}px` } }),
|
|
1511
|
+
visibleItems.map((item, index) => (React.createElement("div", { key: visibleRange.start + index, style: { height: `${itemHeight}px` }, className: "lazy-item" }, renderItem(item, visibleRange.start + index)))),
|
|
1512
|
+
React.createElement("div", { style: {
|
|
1513
|
+
height: `${Math.max(0, containerHeight - (visibleRange.end * itemHeight))}px`
|
|
1514
|
+
} }),
|
|
1515
|
+
isLoading && (React.createElement("div", { className: "lazy-loading" }, "Loading more items..."))));
|
|
1516
|
+
});
|
|
1517
|
+
LazyList.displayName = 'LazyList';
|
|
1518
|
+
|
|
1519
|
+
exports.LazyList = LazyList;
|
|
1520
|
+
exports.useLazyList = useLazyList;
|
|
1521
|
+
//# sourceMappingURL=index.js.map
|