mtrl-addons 0.1.2 → 0.2.2
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/AI.md +28 -230
- package/CLAUDE.md +882 -0
- package/build.js +253 -24
- package/package.json +14 -4
- package/scripts/debug/vlist-selection.ts +121 -0
- package/src/components/index.ts +5 -41
- package/src/components/{list → vlist}/config.ts +66 -95
- package/src/components/vlist/constants.ts +23 -0
- package/src/components/vlist/features/api.ts +626 -0
- package/src/components/vlist/features/index.ts +10 -0
- package/src/components/vlist/features/selection.ts +436 -0
- package/src/components/vlist/features/viewport.ts +59 -0
- package/src/components/vlist/index.ts +17 -0
- package/src/components/{list → vlist}/types.ts +242 -32
- package/src/components/vlist/vlist.ts +92 -0
- package/src/core/compose/features/gestures/index.ts +227 -0
- package/src/core/compose/features/gestures/longpress.ts +383 -0
- package/src/core/compose/features/gestures/pan.ts +424 -0
- package/src/core/compose/features/gestures/pinch.ts +475 -0
- package/src/core/compose/features/gestures/rotate.ts +485 -0
- package/src/core/compose/features/gestures/swipe.ts +492 -0
- package/src/core/compose/features/gestures/tap.ts +334 -0
- package/src/core/compose/features/index.ts +2 -38
- package/src/core/compose/index.ts +13 -29
- package/src/core/gestures/index.ts +31 -0
- package/src/core/gestures/longpress.ts +68 -0
- package/src/core/gestures/manager.ts +418 -0
- package/src/core/gestures/pan.ts +48 -0
- package/src/core/gestures/pinch.ts +58 -0
- package/src/core/gestures/rotate.ts +58 -0
- package/src/core/gestures/swipe.ts +66 -0
- package/src/core/gestures/tap.ts +45 -0
- package/src/core/gestures/types.ts +387 -0
- package/src/core/gestures/utils.ts +128 -0
- package/src/core/index.ts +27 -151
- package/src/core/layout/schema.ts +153 -72
- package/src/core/layout/types.ts +5 -2
- package/src/core/viewport/constants.ts +145 -0
- package/src/core/viewport/features/base.ts +73 -0
- package/src/core/viewport/features/collection.ts +1182 -0
- package/src/core/viewport/features/events.ts +130 -0
- package/src/core/viewport/features/index.ts +20 -0
- package/src/core/{list-manager/features/viewport → viewport/features}/item-size.ts +31 -34
- package/src/core/{list-manager/features/viewport → viewport/features}/loading.ts +4 -4
- package/src/core/viewport/features/momentum.ts +269 -0
- package/src/core/viewport/features/placeholders.ts +335 -0
- package/src/core/viewport/features/rendering.ts +962 -0
- package/src/core/viewport/features/scrollbar.ts +434 -0
- package/src/core/viewport/features/scrolling.ts +634 -0
- package/src/core/viewport/features/utils.ts +94 -0
- package/src/core/viewport/features/virtual.ts +525 -0
- package/src/core/viewport/index.ts +31 -0
- package/src/core/viewport/types.ts +133 -0
- package/src/core/viewport/utils/speed-tracker.ts +79 -0
- package/src/core/viewport/viewport.ts +265 -0
- package/src/index.ts +0 -7
- package/src/styles/components/_vlist.scss +352 -0
- package/src/styles/index.scss +1 -1
- package/test/components/vlist-selection.test.ts +240 -0
- package/test/components/vlist.test.ts +63 -0
- package/test/core/collection/adapter.test.ts +161 -0
- package/bun.lock +0 -792
- package/src/components/list/api.ts +0 -314
- package/src/components/list/constants.ts +0 -56
- package/src/components/list/features/api.ts +0 -428
- package/src/components/list/features/index.ts +0 -31
- package/src/components/list/features/list-manager.ts +0 -502
- package/src/components/list/index.ts +0 -39
- package/src/components/list/list.ts +0 -234
- package/src/core/collection/base-collection.ts +0 -100
- package/src/core/collection/collection-composer.ts +0 -178
- package/src/core/collection/collection.ts +0 -745
- package/src/core/collection/constants.ts +0 -172
- package/src/core/collection/events.ts +0 -428
- package/src/core/collection/features/api/loading.ts +0 -279
- package/src/core/collection/features/operations/data-operations.ts +0 -147
- package/src/core/collection/index.ts +0 -104
- package/src/core/collection/state.ts +0 -497
- package/src/core/collection/types.ts +0 -404
- package/src/core/compose/features/collection.ts +0 -119
- package/src/core/compose/features/selection.ts +0 -213
- package/src/core/compose/features/styling.ts +0 -108
- package/src/core/list-manager/api.ts +0 -599
- package/src/core/list-manager/config.ts +0 -593
- package/src/core/list-manager/constants.ts +0 -268
- package/src/core/list-manager/features/api.ts +0 -58
- package/src/core/list-manager/features/collection/collection.ts +0 -705
- package/src/core/list-manager/features/collection/index.ts +0 -17
- package/src/core/list-manager/features/viewport/constants.ts +0 -42
- package/src/core/list-manager/features/viewport/index.ts +0 -16
- package/src/core/list-manager/features/viewport/placeholders.ts +0 -281
- package/src/core/list-manager/features/viewport/rendering.ts +0 -575
- package/src/core/list-manager/features/viewport/scrollbar.ts +0 -495
- package/src/core/list-manager/features/viewport/scrolling.ts +0 -795
- package/src/core/list-manager/features/viewport/template.ts +0 -220
- package/src/core/list-manager/features/viewport/viewport.ts +0 -654
- package/src/core/list-manager/features/viewport/virtual.ts +0 -309
- package/src/core/list-manager/index.ts +0 -279
- package/src/core/list-manager/list-manager.ts +0 -206
- package/src/core/list-manager/types.ts +0 -439
- package/src/core/list-manager/utils/calculations.ts +0 -290
- package/src/core/list-manager/utils/range-calculator.ts +0 -349
- package/src/core/list-manager/utils/speed-tracker.ts +0 -273
- package/src/styles/components/_list.scss +0 -244
- package/src/types/mtrl.d.ts +0 -6
- package/test/components/list.test.ts +0 -256
- package/test/core/collection/failed-ranges.test.ts +0 -270
- package/test/core/compose/features.test.ts +0 -183
- package/test/core/list-manager/features/collection.test.ts +0 -704
- package/test/core/list-manager/features/viewport.test.ts +0 -698
- package/test/core/list-manager/list-manager.test.ts +0 -593
- package/test/core/list-manager/utils/calculations.test.ts +0 -433
- package/test/core/list-manager/utils/range-calculator.test.ts +0 -569
- package/test/core/list-manager/utils/speed-tracker.test.ts +0 -530
- package/tsconfig.build.json +0 -23
- /package/src/components/{list → vlist}/features.ts +0 -0
- /package/src/core/{compose → viewport}/features/performance.ts +0 -0
|
@@ -1,705 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Collection Feature - Data Management Enhancer
|
|
3
|
-
* Handles speed tracking, range loading, collection integration, and placeholders
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
ListManagerComponent,
|
|
8
|
-
ItemRange,
|
|
9
|
-
SpeedTracker,
|
|
10
|
-
} from "../../types";
|
|
11
|
-
import { LIST_MANAGER_CONSTANTS, PLACEHOLDER } from "../../constants";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Configuration for collection enhancer
|
|
15
|
-
*/
|
|
16
|
-
export interface CollectionConfig {
|
|
17
|
-
collection?: any; // Collection adapter
|
|
18
|
-
rangeSize?: number;
|
|
19
|
-
strategy?: "page" | "offset" | "cursor";
|
|
20
|
-
fastThreshold?: number;
|
|
21
|
-
slowThreshold?: number;
|
|
22
|
-
maskCharacter?: string;
|
|
23
|
-
enablePlaceholders?: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Calculate optimal range size based on viewport dimensions
|
|
28
|
-
* @param containerSize - Viewport container size in pixels
|
|
29
|
-
* @param estimatedItemSize - Estimated item size in pixels
|
|
30
|
-
* @param overscan - Number of items to render outside viewport
|
|
31
|
-
* @returns Optimal range size for data loading
|
|
32
|
-
*/
|
|
33
|
-
function calculateOptimalRangeSize(
|
|
34
|
-
containerSize: number,
|
|
35
|
-
estimatedItemSize: number,
|
|
36
|
-
overscan: number = 5
|
|
37
|
-
): number {
|
|
38
|
-
// Calculate how many items fit in the viewport
|
|
39
|
-
const itemsInViewport = Math.ceil(containerSize / estimatedItemSize);
|
|
40
|
-
|
|
41
|
-
// Add overscan buffer for smooth scrolling
|
|
42
|
-
const withOverscan = itemsInViewport + overscan * 2;
|
|
43
|
-
|
|
44
|
-
// Add preload buffer for better UX (50% more items)
|
|
45
|
-
const withPreload = Math.ceil(withOverscan * 1.5);
|
|
46
|
-
|
|
47
|
-
// Ensure minimum of 10 items and maximum of 100 items per range
|
|
48
|
-
const optimalRangeSize = Math.max(10, Math.min(100, withPreload));
|
|
49
|
-
|
|
50
|
-
return optimalRangeSize;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Component interface after collection enhancement
|
|
55
|
-
*/
|
|
56
|
-
export interface CollectionComponent {
|
|
57
|
-
collection: {
|
|
58
|
-
// Data management
|
|
59
|
-
setItems: (items: any[]) => void;
|
|
60
|
-
getItems: () => any[];
|
|
61
|
-
setTotalItems: (total: number) => void;
|
|
62
|
-
getTotalItems: () => number;
|
|
63
|
-
|
|
64
|
-
// Range management
|
|
65
|
-
loadRange: (offset: number, limit: number) => Promise<any[]>;
|
|
66
|
-
loadMissingRanges: (range: ItemRange) => Promise<void>;
|
|
67
|
-
getLoadedRanges: () => Set<number>;
|
|
68
|
-
getPendingRanges: () => Set<number>;
|
|
69
|
-
getFailedRanges: () => Map<
|
|
70
|
-
number,
|
|
71
|
-
{ attempts: number; lastError: Error; timestamp: number }
|
|
72
|
-
>;
|
|
73
|
-
clearFailedRanges: () => void;
|
|
74
|
-
retryFailedRange: (rangeId: number) => Promise<any[]>;
|
|
75
|
-
|
|
76
|
-
// Pagination strategy
|
|
77
|
-
setPaginationStrategy: (strategy: "page" | "offset" | "cursor") => void;
|
|
78
|
-
getPaginationStrategy: () => "page" | "offset" | "cursor";
|
|
79
|
-
|
|
80
|
-
// Placeholder system
|
|
81
|
-
analyzeDataStructure: (items: any[]) => void;
|
|
82
|
-
generatePlaceholderItem: (index: number) => any;
|
|
83
|
-
showPlaceholders: (count: number) => void;
|
|
84
|
-
getPlaceholderStructure: () => Map<
|
|
85
|
-
string,
|
|
86
|
-
{ min: number; max: number }
|
|
87
|
-
> | null;
|
|
88
|
-
|
|
89
|
-
// State
|
|
90
|
-
isInitialized: () => boolean;
|
|
91
|
-
updateLoadedData: (items: any[], offset: number) => void;
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Adds collection functionality to a List Manager component
|
|
97
|
-
*
|
|
98
|
-
* @param config - Collection configuration
|
|
99
|
-
* @returns Function that enhances a component with collection capabilities
|
|
100
|
-
*/
|
|
101
|
-
export const withCollection =
|
|
102
|
-
(config: CollectionConfig = {}) =>
|
|
103
|
-
<T extends ListManagerComponent>(component: T): T & CollectionComponent => {
|
|
104
|
-
// Configuration with defaults - now uses dynamic range size
|
|
105
|
-
const collection = config.collection;
|
|
106
|
-
|
|
107
|
-
// Calculate optimal range size based on component dimensions
|
|
108
|
-
const containerElement = component.element;
|
|
109
|
-
const containerSize = containerElement?.offsetHeight || 600; // Default to 600px
|
|
110
|
-
const estimatedItemSize = 84; // Default estimated item size
|
|
111
|
-
const overscan = 5; // Default overscan
|
|
112
|
-
|
|
113
|
-
// Use provided rangeSize from config, otherwise calculate optimal range size
|
|
114
|
-
const rangeSize =
|
|
115
|
-
config.rangeSize ||
|
|
116
|
-
calculateOptimalRangeSize(containerSize, estimatedItemSize, overscan);
|
|
117
|
-
|
|
118
|
-
let paginationStrategy: "page" | "offset" | "cursor" =
|
|
119
|
-
config.strategy || "page";
|
|
120
|
-
const maskCharacter =
|
|
121
|
-
config.maskCharacter || LIST_MANAGER_CONSTANTS.PLACEHOLDER.MASK_CHARACTER;
|
|
122
|
-
const enablePlaceholders = config.enablePlaceholders !== false;
|
|
123
|
-
|
|
124
|
-
// Range management
|
|
125
|
-
const loadedRanges = new Set<number>();
|
|
126
|
-
const pendingRanges = new Set<number>();
|
|
127
|
-
const failedRanges = new Map<
|
|
128
|
-
number,
|
|
129
|
-
{ attempts: number; lastError: Error; timestamp: number }
|
|
130
|
-
>();
|
|
131
|
-
|
|
132
|
-
// Track actual pending ranges (not just IDs) for better overlap detection
|
|
133
|
-
const pendingRangeDetails = new Map<
|
|
134
|
-
number,
|
|
135
|
-
{ start: number; end: number }
|
|
136
|
-
>();
|
|
137
|
-
|
|
138
|
-
// Placeholder system
|
|
139
|
-
let placeholderStructure: Map<string, { min: number; max: number }> | null =
|
|
140
|
-
null;
|
|
141
|
-
let placeholderTemplate:
|
|
142
|
-
| ((item: any, index: number) => string | HTMLElement)
|
|
143
|
-
| null = null;
|
|
144
|
-
|
|
145
|
-
// State
|
|
146
|
-
let isCollectionInitialized = false;
|
|
147
|
-
|
|
148
|
-
// Initialize component items array if not already present
|
|
149
|
-
if (!component.items) {
|
|
150
|
-
component.items = [];
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Initialize collection
|
|
155
|
-
*/
|
|
156
|
-
const initialize = (): void => {
|
|
157
|
-
if (isCollectionInitialized) return;
|
|
158
|
-
|
|
159
|
-
// Set template if provided
|
|
160
|
-
if (component.template) {
|
|
161
|
-
placeholderTemplate = component.template;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
isCollectionInitialized = true;
|
|
165
|
-
component.emit?.("collection:initialized", {
|
|
166
|
-
strategy: paginationStrategy,
|
|
167
|
-
rangeSize,
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// 🔥 AUTO-LOAD INITIAL DATA
|
|
171
|
-
if (collection) {
|
|
172
|
-
// Load initial range on next tick to ensure all enhancers are ready
|
|
173
|
-
setTimeout(() => {
|
|
174
|
-
loadRange(0, rangeSize).catch((error) => {
|
|
175
|
-
console.error("❌ [COLLECTION] Initial load failed:", error);
|
|
176
|
-
});
|
|
177
|
-
}, 0);
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Cleanup collection resources
|
|
183
|
-
*/
|
|
184
|
-
const destroy = (): void => {
|
|
185
|
-
// Reset state
|
|
186
|
-
loadedRanges.clear();
|
|
187
|
-
pendingRanges.clear();
|
|
188
|
-
pendingRangeDetails.clear();
|
|
189
|
-
failedRanges.clear();
|
|
190
|
-
placeholderStructure = null;
|
|
191
|
-
component.items = []; // Clear items on destroy
|
|
192
|
-
component.totalItems = 0; // Reset total items
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Check if a range overlaps with any pending ranges
|
|
197
|
-
*/
|
|
198
|
-
const hasOverlappingPendingRange = (
|
|
199
|
-
start: number,
|
|
200
|
-
end: number
|
|
201
|
-
): boolean => {
|
|
202
|
-
for (const [_, range] of pendingRangeDetails) {
|
|
203
|
-
if (start <= range.end && end >= range.start) {
|
|
204
|
-
return true;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return false;
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Load range of items
|
|
212
|
-
*/
|
|
213
|
-
const loadRange = async (offset: number, limit: number): Promise<any[]> => {
|
|
214
|
-
if (!collection) {
|
|
215
|
-
throw new Error("No collection adapter provided");
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const rangeId = Math.floor(offset / rangeSize);
|
|
219
|
-
const rangeEnd = offset + limit - 1;
|
|
220
|
-
|
|
221
|
-
// Check if we already have this data
|
|
222
|
-
if (loadedRanges.has(rangeId)) {
|
|
223
|
-
return [];
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Check for overlapping pending ranges
|
|
227
|
-
if (hasOverlappingPendingRange(offset, rangeEnd)) {
|
|
228
|
-
return [];
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
pendingRanges.add(rangeId);
|
|
232
|
-
pendingRangeDetails.set(rangeId, { start: offset, end: rangeEnd });
|
|
233
|
-
|
|
234
|
-
try {
|
|
235
|
-
let items: any[];
|
|
236
|
-
let responseMeta: any = {};
|
|
237
|
-
|
|
238
|
-
// Adapt to different pagination strategies
|
|
239
|
-
switch (paginationStrategy) {
|
|
240
|
-
case "page":
|
|
241
|
-
// Calculate page number (1-based)
|
|
242
|
-
const page = Math.floor(offset / limit) + 1;
|
|
243
|
-
|
|
244
|
-
// For the last page, ensure we get all remaining items
|
|
245
|
-
const isLastPage = offset + limit >= component.totalItems;
|
|
246
|
-
const adjustedLimit = isLastPage
|
|
247
|
-
? Math.min(limit, component.totalItems - offset)
|
|
248
|
-
: limit;
|
|
249
|
-
|
|
250
|
-
// Adapt generic read method to page-based strategy
|
|
251
|
-
if (collection.loadPage) {
|
|
252
|
-
items = await collection.loadPage({ page, limit: adjustedLimit });
|
|
253
|
-
} else if (collection.read) {
|
|
254
|
-
const result = await collection.read({
|
|
255
|
-
page,
|
|
256
|
-
limit: adjustedLimit,
|
|
257
|
-
});
|
|
258
|
-
// Check if the adapter returned an error
|
|
259
|
-
if (result.error) {
|
|
260
|
-
throw new Error(result.error.message || "Failed to load data");
|
|
261
|
-
}
|
|
262
|
-
items = result.items || [];
|
|
263
|
-
responseMeta = result.meta || {};
|
|
264
|
-
} else {
|
|
265
|
-
throw new Error(
|
|
266
|
-
"Collection adapter missing loadPage or read method"
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
break;
|
|
270
|
-
|
|
271
|
-
case "offset":
|
|
272
|
-
if (collection.loadRange) {
|
|
273
|
-
items = await collection.loadRange({ offset, limit });
|
|
274
|
-
} else if (collection.read) {
|
|
275
|
-
const result = await collection.read({ offset, limit });
|
|
276
|
-
// Check if the adapter returned an error
|
|
277
|
-
if (result.error) {
|
|
278
|
-
throw new Error(result.error.message || "Failed to load data");
|
|
279
|
-
}
|
|
280
|
-
items = result.items || [];
|
|
281
|
-
responseMeta = result.meta || {};
|
|
282
|
-
} else {
|
|
283
|
-
throw new Error(
|
|
284
|
-
"Collection adapter missing loadRange or read method"
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
break;
|
|
288
|
-
|
|
289
|
-
case "cursor":
|
|
290
|
-
// For cursor-based, we need to track the cursor
|
|
291
|
-
if (collection.loadWithCursor) {
|
|
292
|
-
items = await collection.loadWithCursor({
|
|
293
|
-
limit,
|
|
294
|
-
cursor: getCursorForOffset(offset),
|
|
295
|
-
});
|
|
296
|
-
} else if (collection.read) {
|
|
297
|
-
const result = await collection.read({
|
|
298
|
-
cursor: getCursorForOffset(offset),
|
|
299
|
-
limit,
|
|
300
|
-
});
|
|
301
|
-
// Check if the adapter returned an error
|
|
302
|
-
if (result.error) {
|
|
303
|
-
throw new Error(result.error.message || "Failed to load data");
|
|
304
|
-
}
|
|
305
|
-
items = result.items || [];
|
|
306
|
-
responseMeta = result.meta || {};
|
|
307
|
-
} else {
|
|
308
|
-
throw new Error(
|
|
309
|
-
"Collection adapter missing loadWithCursor or read method"
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
break;
|
|
313
|
-
|
|
314
|
-
default:
|
|
315
|
-
throw new Error(
|
|
316
|
-
`Unsupported pagination strategy: ${paginationStrategy}`
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Set total items from API metadata if available (for massive lists)
|
|
321
|
-
if (responseMeta.total && responseMeta.total > component.totalItems) {
|
|
322
|
-
const newTotal = responseMeta.total;
|
|
323
|
-
component.totalItems = newTotal;
|
|
324
|
-
console.log(
|
|
325
|
-
`📊 [COLLECTION] Updated total items from API metadata: ${newTotal.toLocaleString()}`
|
|
326
|
-
);
|
|
327
|
-
component.emit?.("total:changed", { total: newTotal });
|
|
328
|
-
|
|
329
|
-
// Trigger viewport to recalculate total virtual size for massive lists
|
|
330
|
-
if ((component as any).viewport?.updateViewport) {
|
|
331
|
-
(component as any).viewport.updateViewport();
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Analyze structure on first load
|
|
336
|
-
if (enablePlaceholders && !placeholderStructure && items.length > 0) {
|
|
337
|
-
analyzeDataStructure(items);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Update loaded data
|
|
341
|
-
updateLoadedData(items, offset);
|
|
342
|
-
|
|
343
|
-
// Mark range as loaded
|
|
344
|
-
loadedRanges.add(rangeId);
|
|
345
|
-
pendingRanges.delete(rangeId);
|
|
346
|
-
pendingRangeDetails.delete(rangeId);
|
|
347
|
-
|
|
348
|
-
// Emit event
|
|
349
|
-
component.emit?.("range:loaded", {
|
|
350
|
-
range: { start: offset, end: offset + limit - 1 },
|
|
351
|
-
items,
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
return items;
|
|
355
|
-
} catch (error) {
|
|
356
|
-
const errorMessage =
|
|
357
|
-
error instanceof Error ? error.message : "Unknown error";
|
|
358
|
-
console.warn(
|
|
359
|
-
`⚠️ [COLLECTION] Failed to load range ${rangeId} (offset: ${offset}):`,
|
|
360
|
-
errorMessage
|
|
361
|
-
);
|
|
362
|
-
|
|
363
|
-
// Remove from loaded ranges so it can be retried later
|
|
364
|
-
loadedRanges.delete(rangeId);
|
|
365
|
-
|
|
366
|
-
// Track failed range with retry information
|
|
367
|
-
const failedInfo = failedRanges.get(rangeId) || {
|
|
368
|
-
attempts: 0,
|
|
369
|
-
lastError: error as Error,
|
|
370
|
-
timestamp: Date.now(),
|
|
371
|
-
};
|
|
372
|
-
failedInfo.attempts++;
|
|
373
|
-
failedInfo.lastError = error as Error;
|
|
374
|
-
failedInfo.timestamp = Date.now();
|
|
375
|
-
failedRanges.set(rangeId, failedInfo);
|
|
376
|
-
|
|
377
|
-
component.emit?.("range:failed", {
|
|
378
|
-
rangeId,
|
|
379
|
-
offset,
|
|
380
|
-
limit,
|
|
381
|
-
error: error as Error,
|
|
382
|
-
attempts: failedInfo.attempts,
|
|
383
|
-
strategy: paginationStrategy,
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
// Return empty array instead of throwing
|
|
387
|
-
// This prevents error propagation but allows the system to continue
|
|
388
|
-
return [];
|
|
389
|
-
} finally {
|
|
390
|
-
pendingRanges.delete(rangeId);
|
|
391
|
-
pendingRangeDetails.delete(rangeId);
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Load missing ranges based on visible range
|
|
397
|
-
*/
|
|
398
|
-
const loadMissingRanges = async (
|
|
399
|
-
visibleRange: ItemRange
|
|
400
|
-
): Promise<void> => {
|
|
401
|
-
if (!collection) return;
|
|
402
|
-
|
|
403
|
-
// Check if we already have too many pending requests
|
|
404
|
-
if (
|
|
405
|
-
pendingRanges.size >=
|
|
406
|
-
LIST_MANAGER_CONSTANTS.RANGE_LOADING.MAX_CONCURRENT_REQUESTS
|
|
407
|
-
) {
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const startRange = Math.floor(visibleRange.start / rangeSize);
|
|
412
|
-
const endRange = Math.floor(visibleRange.end / rangeSize);
|
|
413
|
-
|
|
414
|
-
const loadPromises: Promise<any[]>[] = [];
|
|
415
|
-
const rangesToLoad: number[] = [];
|
|
416
|
-
|
|
417
|
-
// During fast scrolling, only load the most critical ranges
|
|
418
|
-
const maxRangesToLoad = 3;
|
|
419
|
-
let rangesQueued = 0;
|
|
420
|
-
|
|
421
|
-
for (
|
|
422
|
-
let range = startRange;
|
|
423
|
-
range <= endRange && rangesQueued < maxRangesToLoad;
|
|
424
|
-
range++
|
|
425
|
-
) {
|
|
426
|
-
if (!loadedRanges.has(range) && !pendingRanges.has(range)) {
|
|
427
|
-
// Check if this is a failed range and if we should retry
|
|
428
|
-
const failedInfo = failedRanges.get(range);
|
|
429
|
-
if (failedInfo) {
|
|
430
|
-
const timeSinceLastAttempt = Date.now() - failedInfo.timestamp;
|
|
431
|
-
const backoffTime = Math.min(
|
|
432
|
-
1000 * Math.pow(2, failedInfo.attempts - 1),
|
|
433
|
-
30000
|
|
434
|
-
); // Exponential backoff, max 30s
|
|
435
|
-
|
|
436
|
-
if (timeSinceLastAttempt < backoffTime) {
|
|
437
|
-
// Skip this range, too soon to retry
|
|
438
|
-
continue;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const offset = range * rangeSize;
|
|
443
|
-
rangesToLoad.push(range);
|
|
444
|
-
loadPromises.push(loadRange(offset, rangeSize));
|
|
445
|
-
rangesQueued++;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Load ranges concurrently but respect max concurrent requests
|
|
450
|
-
const maxConcurrent =
|
|
451
|
-
LIST_MANAGER_CONSTANTS.RANGE_LOADING.MAX_CONCURRENT_REQUESTS;
|
|
452
|
-
for (let i = 0; i < loadPromises.length; i += maxConcurrent) {
|
|
453
|
-
const batch = loadPromises.slice(i, i + maxConcurrent);
|
|
454
|
-
await Promise.allSettled(batch);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
component.emit?.("loading:triggered", {
|
|
458
|
-
range: visibleRange,
|
|
459
|
-
strategy: paginationStrategy,
|
|
460
|
-
rangesLoaded: rangesToLoad.length,
|
|
461
|
-
});
|
|
462
|
-
};
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Analyze data structure for placeholder generation
|
|
466
|
-
*/
|
|
467
|
-
const analyzeDataStructure = (items: any[]): void => {
|
|
468
|
-
if (!items.length) return;
|
|
469
|
-
|
|
470
|
-
const structure = new Map<string, { min: number; max: number }>();
|
|
471
|
-
|
|
472
|
-
// Analyze sample of items to find length patterns
|
|
473
|
-
const sampleSize = Math.min(
|
|
474
|
-
items.length,
|
|
475
|
-
PLACEHOLDER.PATTERN_ANALYSIS.SAMPLE_SIZE
|
|
476
|
-
);
|
|
477
|
-
|
|
478
|
-
for (let i = 0; i < sampleSize; i++) {
|
|
479
|
-
const item = items[i];
|
|
480
|
-
if (!item || typeof item !== "object") continue;
|
|
481
|
-
|
|
482
|
-
Object.keys(item).forEach((key) => {
|
|
483
|
-
const value = String(item[key] || "");
|
|
484
|
-
const length = value.length;
|
|
485
|
-
|
|
486
|
-
if (!structure.has(key)) {
|
|
487
|
-
structure.set(key, { min: length, max: length });
|
|
488
|
-
} else {
|
|
489
|
-
const current = structure.get(key)!;
|
|
490
|
-
structure.set(key, {
|
|
491
|
-
min: Math.min(current.min, length),
|
|
492
|
-
max: Math.max(current.max, length),
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
placeholderStructure = structure;
|
|
499
|
-
component.emit?.("structure:analyzed", { structure });
|
|
500
|
-
};
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* Generate placeholder item based on analyzed structure
|
|
504
|
-
*/
|
|
505
|
-
const generatePlaceholderItem = (index: number): any => {
|
|
506
|
-
if (!placeholderStructure) {
|
|
507
|
-
// Fallback placeholder
|
|
508
|
-
return {
|
|
509
|
-
id: `placeholder-${index}`,
|
|
510
|
-
[PLACEHOLDER.PLACEHOLDER_FLAG]: true,
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const placeholder: Record<string, any> = {
|
|
515
|
-
id: `placeholder-${index}`,
|
|
516
|
-
[PLACEHOLDER.PLACEHOLDER_FLAG]: true,
|
|
517
|
-
};
|
|
518
|
-
|
|
519
|
-
// Generate masked values with random lengths within detected ranges
|
|
520
|
-
placeholderStructure.forEach((range, key) => {
|
|
521
|
-
if (LIST_MANAGER_CONSTANTS.PLACEHOLDER.RANDOM_LENGTH_VARIANCE) {
|
|
522
|
-
const length =
|
|
523
|
-
Math.floor(Math.random() * (range.max - range.min + 1)) + range.min;
|
|
524
|
-
placeholder[key] = maskCharacter.repeat(length);
|
|
525
|
-
} else {
|
|
526
|
-
// Use average length
|
|
527
|
-
const length = Math.round((range.min + range.max) / 2);
|
|
528
|
-
placeholder[key] = maskCharacter.repeat(length);
|
|
529
|
-
}
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
return placeholder;
|
|
533
|
-
};
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Show placeholders for a count of items
|
|
537
|
-
*/
|
|
538
|
-
const showPlaceholders = (count: number): void => {
|
|
539
|
-
if (!enablePlaceholders) return;
|
|
540
|
-
|
|
541
|
-
const placeholderItems: any[] = [];
|
|
542
|
-
|
|
543
|
-
for (let i = 0; i < count; i++) {
|
|
544
|
-
const placeholderItem = generatePlaceholderItem(i);
|
|
545
|
-
placeholderItems.push(placeholderItem);
|
|
546
|
-
|
|
547
|
-
// Add to component items array if not already present
|
|
548
|
-
if (!component.items[i]) {
|
|
549
|
-
component.items[i] = placeholderItem;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
component.emit?.("placeholders:shown", {
|
|
554
|
-
count,
|
|
555
|
-
items: placeholderItems,
|
|
556
|
-
});
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
/**
|
|
560
|
-
* Update loaded data in component
|
|
561
|
-
*/
|
|
562
|
-
const updateLoadedData = (items: any[], offset: number): void => {
|
|
563
|
-
// Ensure items array is large enough
|
|
564
|
-
while (component.items.length < offset + items.length) {
|
|
565
|
-
component.items.push(null);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Replace placeholders with real data
|
|
569
|
-
items.forEach((item, index) => {
|
|
570
|
-
const targetIndex = offset + index;
|
|
571
|
-
const existingItem = component.items[targetIndex];
|
|
572
|
-
|
|
573
|
-
// Check if replacing a placeholder
|
|
574
|
-
const wasPlaceholder =
|
|
575
|
-
existingItem && existingItem[PLACEHOLDER.PLACEHOLDER_FLAG];
|
|
576
|
-
|
|
577
|
-
component.items[targetIndex] = item;
|
|
578
|
-
|
|
579
|
-
if (wasPlaceholder) {
|
|
580
|
-
component.emit?.("placeholders:replaced", {
|
|
581
|
-
index: targetIndex,
|
|
582
|
-
item,
|
|
583
|
-
previousPlaceholder: existingItem,
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
// Only update total items if we don't have a large total from API metadata
|
|
589
|
-
// This prevents overriding the massive list total (1,000,000) with loaded items count (20)
|
|
590
|
-
const currentTotal = component.totalItems;
|
|
591
|
-
const loadedTotal = offset + items.length;
|
|
592
|
-
|
|
593
|
-
if (currentTotal < 1000 && loadedTotal > currentTotal) {
|
|
594
|
-
// Only update for small lists where we're building the total incrementally
|
|
595
|
-
component.totalItems = loadedTotal;
|
|
596
|
-
component.emit?.("total:changed", { total: loadedTotal });
|
|
597
|
-
}
|
|
598
|
-
};
|
|
599
|
-
|
|
600
|
-
/**
|
|
601
|
-
* Get cursor for offset (cursor pagination strategy)
|
|
602
|
-
*/
|
|
603
|
-
const getCursorForOffset = (offset: number): string | undefined => {
|
|
604
|
-
// This would need to be implemented based on the specific cursor strategy
|
|
605
|
-
// For now, return undefined to load from beginning
|
|
606
|
-
return undefined;
|
|
607
|
-
};
|
|
608
|
-
|
|
609
|
-
/**
|
|
610
|
-
* Set items directly
|
|
611
|
-
*/
|
|
612
|
-
const setItems = (items: any[]): void => {
|
|
613
|
-
component.items = [...items];
|
|
614
|
-
component.totalItems = items.length;
|
|
615
|
-
|
|
616
|
-
// Analyze structure if enabled and not already done
|
|
617
|
-
if (enablePlaceholders && !placeholderStructure && items.length > 0) {
|
|
618
|
-
analyzeDataStructure(items);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// Mark all items as loaded
|
|
622
|
-
loadedRanges.clear();
|
|
623
|
-
for (let i = 0; i < items.length; i += rangeSize) {
|
|
624
|
-
const rangeId = Math.floor(i / rangeSize);
|
|
625
|
-
loadedRanges.add(rangeId);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
component.emit?.("items:set", { items, total: items.length });
|
|
629
|
-
};
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* Set total items count
|
|
633
|
-
*/
|
|
634
|
-
const setTotalItems = (total: number): void => {
|
|
635
|
-
component.totalItems = total;
|
|
636
|
-
component.emit?.("total:changed", { total });
|
|
637
|
-
};
|
|
638
|
-
|
|
639
|
-
// Initialize collection when component initializes
|
|
640
|
-
const originalInitialize = component.initialize;
|
|
641
|
-
component.initialize = () => {
|
|
642
|
-
originalInitialize.call(component);
|
|
643
|
-
initialize();
|
|
644
|
-
};
|
|
645
|
-
|
|
646
|
-
// Destroy collection when component destroys
|
|
647
|
-
const originalDestroy = component.destroy;
|
|
648
|
-
component.destroy = () => {
|
|
649
|
-
destroy();
|
|
650
|
-
originalDestroy.call(component);
|
|
651
|
-
};
|
|
652
|
-
|
|
653
|
-
// Collection API
|
|
654
|
-
const collectionManager = {
|
|
655
|
-
// Data management
|
|
656
|
-
setItems,
|
|
657
|
-
getItems: () => [...component.items],
|
|
658
|
-
setTotalItems,
|
|
659
|
-
getTotalItems: () => component.totalItems,
|
|
660
|
-
|
|
661
|
-
// Range management
|
|
662
|
-
loadRange,
|
|
663
|
-
loadMissingRanges,
|
|
664
|
-
getLoadedRanges: () => new Set(loadedRanges),
|
|
665
|
-
getPendingRanges: () => new Set(pendingRanges),
|
|
666
|
-
getFailedRanges: () => new Map(failedRanges),
|
|
667
|
-
clearFailedRanges: () => {
|
|
668
|
-
failedRanges.clear();
|
|
669
|
-
},
|
|
670
|
-
retryFailedRange: (rangeId: number) => {
|
|
671
|
-
if (failedRanges.has(rangeId)) {
|
|
672
|
-
failedRanges.delete(rangeId);
|
|
673
|
-
const offset = rangeId * rangeSize;
|
|
674
|
-
return loadRange(offset, rangeSize);
|
|
675
|
-
}
|
|
676
|
-
return Promise.resolve([]);
|
|
677
|
-
},
|
|
678
|
-
|
|
679
|
-
// Pagination strategy
|
|
680
|
-
setPaginationStrategy: (strategy: "page" | "offset" | "cursor") => {
|
|
681
|
-
paginationStrategy = strategy;
|
|
682
|
-
// Reset loaded ranges when strategy changes
|
|
683
|
-
loadedRanges.clear();
|
|
684
|
-
pendingRanges.clear();
|
|
685
|
-
component.emit?.("strategy:changed", { strategy });
|
|
686
|
-
},
|
|
687
|
-
getPaginationStrategy: () => paginationStrategy,
|
|
688
|
-
|
|
689
|
-
// Placeholder system
|
|
690
|
-
analyzeDataStructure,
|
|
691
|
-
generatePlaceholderItem,
|
|
692
|
-
showPlaceholders,
|
|
693
|
-
getPlaceholderStructure: () =>
|
|
694
|
-
placeholderStructure ? new Map(placeholderStructure) : null,
|
|
695
|
-
|
|
696
|
-
// State
|
|
697
|
-
isInitialized: () => isCollectionInitialized,
|
|
698
|
-
updateLoadedData,
|
|
699
|
-
};
|
|
700
|
-
|
|
701
|
-
return {
|
|
702
|
-
...component,
|
|
703
|
-
collection: collectionManager,
|
|
704
|
-
};
|
|
705
|
-
};
|