mtrl-addons 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/{src/components/index.ts → dist/components/index.d.ts} +0 -2
  2. package/dist/components/vlist/config.d.ts +86 -0
  3. package/{src/components/vlist/constants.ts → dist/components/vlist/constants.d.ts} +10 -11
  4. package/dist/components/vlist/features/api.d.ts +7 -0
  5. package/{src/components/vlist/features/index.ts → dist/components/vlist/features/index.d.ts} +0 -2
  6. package/dist/components/vlist/features/selection.d.ts +6 -0
  7. package/dist/components/vlist/features/viewport.d.ts +9 -0
  8. package/dist/components/vlist/features.d.ts +31 -0
  9. package/{src/components/vlist/index.ts → dist/components/vlist/index.d.ts} +1 -9
  10. package/dist/components/vlist/types.d.ts +596 -0
  11. package/dist/components/vlist/vlist.d.ts +29 -0
  12. package/dist/core/compose/features/gestures/index.d.ts +86 -0
  13. package/dist/core/compose/features/gestures/longpress.d.ts +85 -0
  14. package/dist/core/compose/features/gestures/pan.d.ts +108 -0
  15. package/dist/core/compose/features/gestures/pinch.d.ts +111 -0
  16. package/dist/core/compose/features/gestures/rotate.d.ts +111 -0
  17. package/dist/core/compose/features/gestures/swipe.d.ts +149 -0
  18. package/dist/core/compose/features/gestures/tap.d.ts +79 -0
  19. package/{src/core/compose/features/index.ts → dist/core/compose/features/index.d.ts} +1 -2
  20. package/{src/core/compose/index.ts → dist/core/compose/index.d.ts} +2 -11
  21. package/{src/core/gestures/index.ts → dist/core/gestures/index.d.ts} +1 -20
  22. package/dist/core/gestures/longpress.d.ts +23 -0
  23. package/dist/core/gestures/manager.d.ts +14 -0
  24. package/dist/core/gestures/pan.d.ts +12 -0
  25. package/dist/core/gestures/pinch.d.ts +14 -0
  26. package/dist/core/gestures/rotate.d.ts +14 -0
  27. package/dist/core/gestures/swipe.d.ts +20 -0
  28. package/dist/core/gestures/tap.d.ts +12 -0
  29. package/dist/core/gestures/types.d.ts +320 -0
  30. package/dist/core/gestures/utils.d.ts +57 -0
  31. package/dist/core/index.d.ts +13 -0
  32. package/dist/core/layout/config.d.ts +33 -0
  33. package/dist/core/layout/index.d.ts +51 -0
  34. package/dist/core/layout/jsx.d.ts +65 -0
  35. package/dist/core/layout/schema.d.ts +112 -0
  36. package/dist/core/layout/types.d.ts +69 -0
  37. package/dist/core/viewport/constants.d.ts +105 -0
  38. package/dist/core/viewport/features/base.d.ts +14 -0
  39. package/dist/core/viewport/features/collection.d.ts +41 -0
  40. package/dist/core/viewport/features/events.d.ts +13 -0
  41. package/{src/core/viewport/features/index.ts → dist/core/viewport/features/index.d.ts} +0 -7
  42. package/dist/core/viewport/features/item-size.d.ts +30 -0
  43. package/dist/core/viewport/features/loading.d.ts +34 -0
  44. package/dist/core/viewport/features/momentum.d.ts +17 -0
  45. package/dist/core/viewport/features/performance.d.ts +53 -0
  46. package/dist/core/viewport/features/placeholders.d.ts +38 -0
  47. package/dist/core/viewport/features/rendering.d.ts +16 -0
  48. package/dist/core/viewport/features/scrollbar.d.ts +26 -0
  49. package/dist/core/viewport/features/scrolling.d.ts +16 -0
  50. package/dist/core/viewport/features/utils.d.ts +43 -0
  51. package/dist/core/viewport/features/virtual.d.ts +18 -0
  52. package/{src/core/viewport/index.ts → dist/core/viewport/index.d.ts} +1 -17
  53. package/dist/core/viewport/types.d.ts +96 -0
  54. package/dist/core/viewport/utils/speed-tracker.d.ts +22 -0
  55. package/dist/core/viewport/viewport.d.ts +11 -0
  56. package/{src/index.ts → dist/index.d.ts} +0 -4
  57. package/dist/index.js +5143 -0
  58. package/dist/index.mjs +5111 -0
  59. package/dist/styles.css +254 -0
  60. package/dist/styles.css.map +1 -0
  61. package/package.json +6 -1
  62. package/src/styles/components/_vlist.scss +234 -213
  63. package/.cursorrules +0 -117
  64. package/AI.md +0 -241
  65. package/build.js +0 -201
  66. package/scripts/analyze-orphaned-functions.ts +0 -387
  67. package/scripts/debug/vlist-selection.ts +0 -121
  68. package/src/components/vlist/config.ts +0 -323
  69. package/src/components/vlist/features/api.ts +0 -322
  70. package/src/components/vlist/features/selection.ts +0 -444
  71. package/src/components/vlist/features/viewport.ts +0 -65
  72. package/src/components/vlist/features.ts +0 -112
  73. package/src/components/vlist/types.ts +0 -591
  74. package/src/components/vlist/vlist.ts +0 -92
  75. package/src/core/compose/features/gestures/index.ts +0 -227
  76. package/src/core/compose/features/gestures/longpress.ts +0 -383
  77. package/src/core/compose/features/gestures/pan.ts +0 -424
  78. package/src/core/compose/features/gestures/pinch.ts +0 -475
  79. package/src/core/compose/features/gestures/rotate.ts +0 -485
  80. package/src/core/compose/features/gestures/swipe.ts +0 -492
  81. package/src/core/compose/features/gestures/tap.ts +0 -334
  82. package/src/core/gestures/longpress.ts +0 -68
  83. package/src/core/gestures/manager.ts +0 -418
  84. package/src/core/gestures/pan.ts +0 -48
  85. package/src/core/gestures/pinch.ts +0 -58
  86. package/src/core/gestures/rotate.ts +0 -58
  87. package/src/core/gestures/swipe.ts +0 -66
  88. package/src/core/gestures/tap.ts +0 -45
  89. package/src/core/gestures/types.ts +0 -387
  90. package/src/core/gestures/utils.ts +0 -128
  91. package/src/core/index.ts +0 -43
  92. package/src/core/layout/config.ts +0 -102
  93. package/src/core/layout/index.ts +0 -168
  94. package/src/core/layout/jsx.ts +0 -174
  95. package/src/core/layout/schema.ts +0 -1001
  96. package/src/core/layout/types.ts +0 -95
  97. package/src/core/viewport/constants.ts +0 -140
  98. package/src/core/viewport/features/base.ts +0 -73
  99. package/src/core/viewport/features/collection.ts +0 -882
  100. package/src/core/viewport/features/events.ts +0 -130
  101. package/src/core/viewport/features/item-size.ts +0 -271
  102. package/src/core/viewport/features/loading.ts +0 -263
  103. package/src/core/viewport/features/momentum.ts +0 -260
  104. package/src/core/viewport/features/performance.ts +0 -161
  105. package/src/core/viewport/features/placeholders.ts +0 -335
  106. package/src/core/viewport/features/rendering.ts +0 -568
  107. package/src/core/viewport/features/scrollbar.ts +0 -434
  108. package/src/core/viewport/features/scrolling.ts +0 -618
  109. package/src/core/viewport/features/utils.ts +0 -88
  110. package/src/core/viewport/features/virtual.ts +0 -384
  111. package/src/core/viewport/types.ts +0 -133
  112. package/src/core/viewport/utils/speed-tracker.ts +0 -79
  113. package/src/core/viewport/viewport.ts +0 -246
  114. package/test/benchmarks/layout/advanced.test.ts +0 -656
  115. package/test/benchmarks/layout/comparison.test.ts +0 -519
  116. package/test/benchmarks/layout/performance-comparison.test.ts +0 -274
  117. package/test/benchmarks/layout/real-components.test.ts +0 -733
  118. package/test/benchmarks/layout/simple.test.ts +0 -321
  119. package/test/benchmarks/layout/stress.test.ts +0 -990
  120. package/test/collection/basic.test.ts +0 -304
  121. package/test/components/vlist-selection.test.ts +0 -240
  122. package/test/components/vlist.test.ts +0 -63
  123. package/test/core/collection/adapter.test.ts +0 -161
  124. package/test/core/collection/collection.test.ts +0 -394
  125. package/test/core/layout/layout.test.ts +0 -201
  126. package/test/utils/dom-helpers.ts +0 -275
  127. package/test/utils/performance-helpers.ts +0 -392
  128. package/tsconfig.json +0 -20
@@ -1,882 +0,0 @@
1
- // src/core/viewport/features/collection.ts
2
-
3
- /**
4
- * Collection Feature - Data management and range loading
5
- * Handles collection integration, pagination, and data fetching
6
- */
7
-
8
- import type { ViewportContext, ViewportComponent } from "../types";
9
- import { VIEWPORT_CONSTANTS } from "../constants";
10
-
11
- export interface CollectionConfig {
12
- collection?: any; // Collection adapter
13
- rangeSize?: number; // Default range size for loading
14
- strategy?: "offset" | "page" | "cursor"; // Loading strategy
15
- transform?: (item: any) => any; // Item transformation function
16
- cancelLoadThreshold?: number; // Velocity threshold for cancelling loads
17
- maxConcurrentRequests?: number;
18
- enableRequestQueue?: boolean;
19
- maxQueueSize?: number;
20
- loadOnDragEnd?: boolean; // Enable loading when drag ends (safety measure)
21
- }
22
-
23
- export interface CollectionComponent {
24
- collection: {
25
- loadRange: (offset: number, limit: number) => Promise<any[]>;
26
- loadMissingRanges: (
27
- range: { start: number; end: number },
28
- caller?: string
29
- ) => Promise<void>;
30
- getLoadedRanges: () => Set<number>;
31
- getPendingRanges: () => Set<number>;
32
- clearFailedRanges: () => void;
33
- retryFailedRange: (rangeId: number) => Promise<any[]>;
34
- setTotalItems: (total: number) => void;
35
- getTotalItems: () => number;
36
- // Cursor-specific methods
37
- getCurrentCursor: () => string | null;
38
- getCursorForPage: (page: number) => string | null;
39
- };
40
- }
41
-
42
- /**
43
- * Adds collection functionality to viewport component
44
- */
45
- export function withCollection(config: CollectionConfig = {}) {
46
- return <T extends ViewportContext & ViewportComponent>(
47
- component: T
48
- ): T & CollectionComponent => {
49
- const {
50
- collection,
51
- rangeSize = VIEWPORT_CONSTANTS.LOADING.DEFAULT_RANGE_SIZE,
52
- strategy = "offset",
53
- transform,
54
- cancelLoadThreshold = VIEWPORT_CONSTANTS.LOADING.CANCEL_THRESHOLD,
55
- maxConcurrentRequests = VIEWPORT_CONSTANTS.LOADING
56
- .MAX_CONCURRENT_REQUESTS,
57
- enableRequestQueue = VIEWPORT_CONSTANTS.REQUEST_QUEUE.ENABLED,
58
- maxQueueSize = VIEWPORT_CONSTANTS.REQUEST_QUEUE.MAX_QUEUE_SIZE,
59
- loadOnDragEnd = true, // Default to true as safety measure
60
- } = config;
61
-
62
- // console.log("[Viewport Collection] Initialized with strategy:", strategy);
63
-
64
- // Loading manager state
65
- interface QueuedRequest {
66
- range: { start: number; end: number };
67
- priority: "high" | "normal" | "low";
68
- timestamp: number;
69
- resolve: () => void;
70
- reject: (error: any) => void;
71
- }
72
-
73
- // State tracking
74
- let currentVelocity = 0;
75
- let activeLoadCount = 0;
76
- let completedLoads = 0;
77
- let failedLoads = 0;
78
- let cancelledLoads = 0;
79
- let isDragging = false; // Track drag state
80
-
81
- const activeLoadRanges = new Set<string>();
82
- let loadRequestQueue: QueuedRequest[] = [];
83
-
84
- // State
85
- let items: any[] = [];
86
- let totalItems = 0;
87
- let loadedRanges = new Set<number>();
88
- let pendingRanges = new Set<number>();
89
- let failedRanges = new Map<
90
- number,
91
- {
92
- attempts: number;
93
- lastError: Error;
94
- timestamp: number;
95
- }
96
- >();
97
- let activeRequests = new Map<number, Promise<any[]>>();
98
-
99
- // Cursor pagination state
100
- let currentCursor: string | null = null;
101
- let cursorMap = new Map<number, string>(); // Map page number to cursor
102
- let pageToOffsetMap = new Map<number, number>(); // Map page to actual offset
103
- let highestLoadedPage = 0;
104
- let discoveredTotal: number | null = null; // Track discovered total from API
105
- let hasReachedEnd = false; // Track if we've reached the end of data
106
-
107
- // Share items array with component
108
- component.items = items;
109
-
110
- /**
111
- * Get a unique ID for a range based on offset and the base range size
112
- */
113
- const getRangeId = (offset: number, limit: number): number => {
114
- // Always use the base rangeSize for consistent IDs
115
- // This ensures merged ranges can be tracked properly
116
- return Math.floor(offset / rangeSize);
117
- };
118
-
119
- // Loading manager helpers
120
- const getRangeKey = (range: { start: number; end: number }): string => {
121
- return `${range.start}-${range.end}`;
122
- };
123
-
124
- const canLoad = (): boolean => {
125
- return currentVelocity <= cancelLoadThreshold;
126
- };
127
-
128
- const processQueue = () => {
129
- if (!enableRequestQueue || loadRequestQueue.length === 0) return;
130
-
131
- // Sort queue by priority and timestamp
132
- loadRequestQueue.sort((a, b) => {
133
- const priorityOrder = { high: 0, normal: 1, low: 2 };
134
- const priorityDiff =
135
- priorityOrder[a.priority] - priorityOrder[b.priority];
136
- return priorityDiff !== 0 ? priorityDiff : a.timestamp - b.timestamp;
137
- });
138
-
139
- // Process requests up to capacity
140
- while (
141
- loadRequestQueue.length > 0 &&
142
- activeLoadCount < maxConcurrentRequests
143
- ) {
144
- const request = loadRequestQueue.shift();
145
- if (request) {
146
- executeQueuedLoad(request);
147
- }
148
- }
149
- };
150
-
151
- const executeQueuedLoad = (request: QueuedRequest) => {
152
- activeLoadCount++;
153
- activeLoadRanges.add(getRangeKey(request.range));
154
-
155
- // console.log(
156
- // `[LoadingManager] Executing load for range ${request.range.start}-${
157
- // request.range.end
158
- // } (velocity: ${currentVelocity.toFixed(2)})`
159
- // );
160
-
161
- // Call the actual loadMissingRanges function
162
- loadMissingRangesInternal(request.range)
163
- .then(() => {
164
- request.resolve();
165
- completedLoads++;
166
- })
167
- .catch((error: Error) => {
168
- request.reject(error);
169
- failedLoads++;
170
- })
171
- .finally(() => {
172
- activeLoadCount--;
173
- activeLoadRanges.delete(getRangeKey(request.range));
174
- processQueue();
175
- });
176
- };
177
-
178
- /**
179
- * Transform items if transform function provided
180
- */
181
- const transformItems = (rawItems: any[]): any[] => {
182
- if (!transform) return rawItems;
183
- return rawItems.map(transform);
184
- };
185
-
186
- /**
187
- * Load a range of data
188
- */
189
- const loadRange = async (offset: number, limit: number): Promise<any[]> => {
190
- // console.log(
191
- // `[Collection] loadRange called: offset=${offset}, limit=${limit}`
192
- // );
193
-
194
- if (!collection) {
195
- console.warn("[Collection] No collection adapter configured");
196
- return [];
197
- }
198
-
199
- const rangeId = getRangeId(offset, limit);
200
- //console.log(`[Collection] Range ID: ${rangeId}`);
201
-
202
- // Check if already loaded
203
- if (loadedRanges.has(rangeId)) {
204
- // console.log(
205
- // `[Collection] Range ${rangeId} already loaded, returning cached data`
206
- // );
207
- return items.slice(offset, offset + limit);
208
- }
209
-
210
- // Check if already pending
211
- if (pendingRanges.has(rangeId)) {
212
- // console.log(`[Collection] Range ${rangeId} already pending`);
213
- const existingRequest = activeRequests.get(rangeId);
214
- if (existingRequest) {
215
- // console.log(
216
- // `[Collection] Returning existing request for range ${rangeId}`
217
- // );
218
- return existingRequest;
219
- }
220
- }
221
-
222
- // Mark as pending
223
- // console.log(
224
- // `[Collection] Marking range ${rangeId} as pending and loading...`
225
- // );
226
- pendingRanges.add(rangeId);
227
-
228
- // Create request promise
229
- const requestPromise = (async () => {
230
- try {
231
- // Call collection adapter with appropriate parameters
232
- const page = Math.floor(offset / limit) + 1;
233
- let params: any;
234
-
235
- if (strategy === "cursor") {
236
- // For cursor pagination
237
- if (page === 1) {
238
- // First page - no cursor
239
- params = { limit };
240
- } else {
241
- // Check if we have cursor for previous page
242
- const prevPageCursor = cursorMap.get(page - 1);
243
- if (!prevPageCursor) {
244
- // Can't load this page without previous cursor
245
- console.warn(
246
- `[Collection] Cannot load page ${page} without cursor for page ${
247
- page - 1
248
- }`
249
- );
250
- throw new Error(
251
- `Sequential loading required - missing cursor for page ${
252
- page - 1
253
- }`
254
- );
255
- }
256
- params = { cursor: prevPageCursor, limit };
257
- }
258
- } else if (strategy === "page") {
259
- params = { page, limit };
260
- } else {
261
- // offset strategy
262
- params = { offset, limit };
263
- }
264
-
265
- // console.log(
266
- // `[Viewport Collection] Loading range offset=${offset}, limit=${limit}, strategy=${strategy}, calculated page=${page}, params:`,
267
- // JSON.stringify(params)
268
- // );
269
-
270
- const response = await collection.read(params);
271
-
272
- // Extract items and total
273
- const rawItems = response.data || response.items || response;
274
- const meta = response.meta || {};
275
-
276
- // For cursor pagination, track the cursor (check both cursor and nextCursor)
277
- const responseCursor = meta.cursor || meta.nextCursor;
278
- if (strategy === "cursor" && responseCursor) {
279
- currentCursor = responseCursor;
280
- cursorMap.set(page, responseCursor);
281
- pageToOffsetMap.set(page, offset);
282
- highestLoadedPage = Math.max(highestLoadedPage, page);
283
- console.log(
284
- `[Collection] Stored cursor for page ${page}: ${responseCursor}`
285
- );
286
- }
287
-
288
- // Check if we've reached the end
289
- if (strategy === "cursor" && meta.hasNext === false) {
290
- hasReachedEnd = true;
291
- console.log(
292
- `[Collection] Reached end of cursor pagination at page ${page}`
293
- );
294
- }
295
-
296
- // Update discovered total if provided
297
- if (meta.total !== undefined) {
298
- discoveredTotal = meta.total;
299
- }
300
-
301
- // Transform items
302
- const transformedItems = transformItems(rawItems);
303
-
304
- // Add items to array
305
- transformedItems.forEach((item, index) => {
306
- items[offset + index] = item;
307
- });
308
-
309
- // For cursor strategy, calculate dynamic total based on loaded data
310
- let newTotal = discoveredTotal || totalItems;
311
-
312
- if (strategy === "cursor") {
313
- // Calculate total based on loaded items + margin
314
- const loadedItemsCount = items.filter(
315
- (item) => item !== undefined
316
- ).length;
317
- const marginItems = hasReachedEnd
318
- ? 0
319
- : rangeSize *
320
- VIEWPORT_CONSTANTS.PAGINATION.CURSOR_SCROLL_MARGIN_MULTIPLIER;
321
- const minVirtualItems =
322
- rangeSize *
323
- VIEWPORT_CONSTANTS.PAGINATION.CURSOR_MIN_VIRTUAL_SIZE_MULTIPLIER;
324
-
325
- // Dynamic total: loaded items + margin (unless we've reached the end)
326
- newTotal = Math.max(
327
- loadedItemsCount + marginItems,
328
- minVirtualItems
329
- );
330
-
331
- console.log(
332
- `[Collection] Cursor mode virtual size: loaded=${loadedItemsCount}, margin=${marginItems}, total=${newTotal}, hasReachedEnd=${hasReachedEnd}`
333
- );
334
-
335
- // Update total if it has grown
336
- if (newTotal > totalItems) {
337
- console.log(
338
- `[Collection] Updating cursor virtual size from ${totalItems} to ${newTotal}`
339
- );
340
- totalItems = newTotal;
341
- setTotalItems(newTotal);
342
- }
343
- } else {
344
- // For other strategies, use discovered total or current total
345
- newTotal = discoveredTotal || totalItems;
346
- }
347
-
348
- // Update state
349
- if (newTotal !== totalItems) {
350
- totalItems = newTotal;
351
- setTotalItems(newTotal);
352
- }
353
-
354
- // Update component items reference
355
- component.items = items;
356
-
357
- // Emit items changed event
358
- component.emit?.("viewport:items-changed", {
359
- totalItems: newTotal,
360
- loadedCount: items.filter((item) => item !== undefined).length,
361
- });
362
-
363
- // Update viewport state
364
- const viewportState = (component.viewport as any).state;
365
- if (viewportState) {
366
- viewportState.totalItems = newTotal || items.length;
367
- }
368
-
369
- // Mark as loaded
370
- loadedRanges.add(rangeId);
371
- pendingRanges.delete(rangeId);
372
- failedRanges.delete(rangeId);
373
-
374
- // console.log(
375
- // `[Collection] Range ${rangeId} loaded successfully with ${transformedItems.length} items`
376
- // );
377
-
378
- // Emit events
379
- component.emit?.("viewport:range-loaded", {
380
- offset,
381
- limit,
382
- items: transformedItems,
383
- total: newTotal,
384
- });
385
-
386
- // Emit collection loaded event with more details
387
- component.emit?.("collection:range-loaded", {
388
- offset,
389
- limit,
390
- items: transformedItems,
391
- total: newTotal,
392
- rangeId,
393
- itemsInMemory: items.filter((item) => item !== undefined).length,
394
- cursor: strategy === "cursor" ? currentCursor : undefined,
395
- });
396
-
397
- // Trigger viewport update
398
- component.viewport?.updateViewport?.();
399
-
400
- return transformedItems;
401
- } catch (error) {
402
- // Handle error
403
- pendingRanges.delete(rangeId);
404
-
405
- const attempts = (failedRanges.get(rangeId)?.attempts || 0) + 1;
406
- failedRanges.set(rangeId, {
407
- attempts,
408
- lastError: error as Error,
409
- timestamp: Date.now(),
410
- });
411
-
412
- // Emit error event
413
- component.emit?.("viewport:range-error", {
414
- offset,
415
- limit,
416
- error,
417
- attempts,
418
- });
419
-
420
- throw error;
421
- } finally {
422
- activeRequests.delete(rangeId);
423
- }
424
- })();
425
-
426
- activeRequests.set(rangeId, requestPromise);
427
- return requestPromise;
428
- };
429
-
430
- /**
431
- * Load missing ranges from collection
432
- */
433
- const loadMissingRangesInternal = async (range: {
434
- start: number;
435
- end: number;
436
- }): Promise<void> => {
437
- if (!collection) return;
438
-
439
- // For cursor pagination, we need to load sequentially
440
- if (strategy === "cursor") {
441
- const startPage = Math.floor(range.start / rangeSize) + 1;
442
- const endPage = Math.floor(range.end / rangeSize) + 1;
443
-
444
- // Limit how many pages we'll load at once
445
- const maxPagesToLoad = 10;
446
- const currentHighestPage = highestLoadedPage || 0;
447
- const limitedEndPage = Math.min(
448
- endPage,
449
- currentHighestPage + maxPagesToLoad
450
- );
451
-
452
- console.log(
453
- `[Collection] Cursor mode: need to load pages ${startPage} to ${endPage}, limited to ${limitedEndPage}`
454
- );
455
-
456
- // Check if we need to load pages sequentially
457
- for (let page = startPage; page <= limitedEndPage; page++) {
458
- const rangeId = page - 1; // Convert to 0-based rangeId
459
- const offset = rangeId * rangeSize;
460
-
461
- // Skip if already being loaded
462
- if (pendingRanges.has(rangeId)) {
463
- // Range already being loaded
464
- return;
465
- }
466
-
467
- if (!loadedRanges.has(rangeId) && !pendingRanges.has(rangeId)) {
468
- // For cursor pagination, we must load sequentially
469
- // Check if we have all previous pages loaded
470
- if (page > 1) {
471
- let canLoad = true;
472
- for (let prevPage = 1; prevPage < page; prevPage++) {
473
- const prevRangeId = prevPage - 1;
474
- if (!loadedRanges.has(prevRangeId)) {
475
- console.log(
476
- `[Collection] Cannot load page ${page} - need to load page ${prevPage} first`
477
- );
478
- canLoad = false;
479
-
480
- // Try to load the missing page
481
- if (!pendingRanges.has(prevRangeId)) {
482
- try {
483
- await loadRange(prevRangeId * rangeSize, rangeSize);
484
- } catch (error) {
485
- console.error(
486
- `[Collection] Failed to load prerequisite page ${prevPage}:`,
487
- error
488
- );
489
- return; // Stop trying to load further pages
490
- }
491
- }
492
- break;
493
- }
494
- }
495
-
496
- if (!canLoad) {
497
- continue; // Skip this page for now
498
- }
499
- }
500
-
501
- try {
502
- await loadRange(offset, rangeSize);
503
- } catch (error) {
504
- console.error(`[Collection] Failed to load page ${page}:`, error);
505
- break; // Stop sequential loading on error
506
- }
507
- }
508
- }
509
-
510
- if (endPage > limitedEndPage) {
511
- console.log(
512
- `[Collection] Stopped at page ${limitedEndPage} to prevent excessive loading (requested up to ${endPage})`
513
- );
514
- }
515
-
516
- return;
517
- }
518
-
519
- // Original logic for offset/page strategies
520
- // Calculate range boundaries
521
- const startRange = Math.floor(range.start / rangeSize);
522
- const endRange = Math.floor(range.end / rangeSize);
523
-
524
- // Collect ranges that need loading
525
- const rangesToLoad: number[] = [];
526
- for (let rangeId = startRange; rangeId <= endRange; rangeId++) {
527
- if (!loadedRanges.has(rangeId) && !pendingRanges.has(rangeId)) {
528
- rangesToLoad.push(rangeId);
529
- }
530
- }
531
-
532
- if (rangesToLoad.length === 0) {
533
- // All ranges are already loaded or pending
534
- // Check if there are queued requests we should process
535
- if (
536
- loadRequestQueue.length > 0 &&
537
- activeLoadCount < maxConcurrentRequests
538
- ) {
539
- processQueue();
540
- }
541
- return;
542
- }
543
-
544
- // Load ranges individually - no merging to avoid loading old ranges
545
- const promises = rangesToLoad.map((rangeId) =>
546
- loadRange(rangeId * rangeSize, rangeSize)
547
- );
548
- await Promise.allSettled(promises);
549
- };
550
-
551
- /**
552
- * Velocity-aware wrapper for loadMissingRanges
553
- */
554
- const loadMissingRanges = (
555
- range: {
556
- start: number;
557
- end: number;
558
- },
559
- caller?: string
560
- ): Promise<void> => {
561
- return new Promise((resolve, reject) => {
562
- const rangeKey = getRangeKey(range);
563
- // Removed noisy log
564
-
565
- // Check if already loading
566
- if (activeLoadRanges.has(rangeKey)) {
567
- // Range already being loaded
568
- resolve();
569
- return;
570
- }
571
-
572
- // Skip if dragging with low velocity (but not if we're idle)
573
- if (isDragging && currentVelocity < 0.5 && currentVelocity > 0) {
574
- // console.log(
575
- // "[Collection] Load skipped - actively dragging with low velocity"
576
- // );
577
- cancelledLoads++;
578
- resolve();
579
- return;
580
- }
581
-
582
- // Check velocity - if too high, cancel the request entirely
583
- if (!canLoad()) {
584
- // console.log(
585
- // `[Collection] Load cancelled - velocity ${currentVelocity.toFixed(
586
- // 2
587
- // )} exceeds threshold ${cancelLoadThreshold}`
588
- // );
589
- cancelledLoads++;
590
- resolve();
591
- return;
592
- }
593
-
594
- // Check capacity
595
- if (activeLoadCount < maxConcurrentRequests) {
596
- // Execute immediately
597
- executeQueuedLoad({
598
- range,
599
- priority: "normal",
600
- timestamp: Date.now(),
601
- resolve,
602
- reject,
603
- });
604
- } else if (
605
- enableRequestQueue &&
606
- loadRequestQueue.length < maxQueueSize
607
- ) {
608
- // Queue the request
609
- loadRequestQueue.push({
610
- range,
611
- priority: "normal",
612
- timestamp: Date.now(),
613
- resolve,
614
- reject,
615
- });
616
- // console.log(
617
- // `[LoadingManager] Queued request (at capacity), queue size: ${loadRequestQueue.length}`
618
- // );
619
- } else {
620
- // Queue overflow - resolve to avoid errors
621
- if (loadRequestQueue.length >= maxQueueSize) {
622
- const removed = loadRequestQueue.splice(
623
- 0,
624
- loadRequestQueue.length - maxQueueSize
625
- );
626
- removed.forEach((r) => {
627
- cancelledLoads++;
628
- r.resolve();
629
- });
630
- }
631
- resolve();
632
- }
633
- });
634
- };
635
-
636
- /**
637
- * Retry a failed range
638
- */
639
- const retryFailedRange = async (rangeId: number): Promise<any[]> => {
640
- failedRanges.delete(rangeId);
641
- const offset = rangeId * rangeSize;
642
- return loadRange(offset, rangeSize);
643
- };
644
-
645
- /**
646
- * Set total items count
647
- */
648
- const setTotalItems = (total: number): void => {
649
- totalItems = total;
650
-
651
- // Update viewport state's total items
652
- const viewportState = (component.viewport as any).state;
653
- if (viewportState) {
654
- viewportState.totalItems = total;
655
- }
656
-
657
- component.emit?.("viewport:total-items-changed", { total });
658
- };
659
-
660
- // Hook into viewport initialization
661
- const originalInitialize = component.viewport.initialize;
662
- component.viewport.initialize = () => {
663
- originalInitialize();
664
-
665
- // Set initial total if provided
666
- if (component.totalItems) {
667
- totalItems = component.totalItems;
668
- }
669
-
670
- // Listen for drag events
671
- component.on?.("viewport:drag-start", () => {
672
- isDragging = true;
673
- });
674
-
675
- component.on?.("viewport:drag-end", () => {
676
- isDragging = false;
677
- // Process any queued requests after drag ends (safety measure)
678
- // This ensures placeholders are replaced even if idle detection fails
679
- if (loadOnDragEnd) {
680
- processQueue();
681
- }
682
- });
683
-
684
- // Listen for range changes
685
- component.on?.("viewport:range-changed", async (data: any) => {
686
- // Don't load during fast scrolling - loadMissingRanges will handle velocity check
687
-
688
- // Skip initial load if we're dragging and velocity is low (drag just started)
689
- // if (isDragging && currentVelocity < 0.5) {
690
- // console.log(
691
- // "[Collection] Skipping range-changed load during drag start"
692
- // );
693
- // return;
694
- // }
695
-
696
- // Extract range from event data - virtual feature emits { range: { start, end }, scrollPosition }
697
- const range = data.range || data;
698
- const { start, end } = range;
699
-
700
- // Validate range before loading
701
- if (typeof start !== "number" || typeof end !== "number") {
702
- console.warn(
703
- "[Collection] Invalid range in viewport:range-changed event:",
704
- data
705
- );
706
- return;
707
- }
708
-
709
- // Load missing ranges if needed
710
- await loadMissingRanges({ start, end }, "viewport:range-changed");
711
-
712
- // For cursor mode, check if we need to update virtual size
713
- if (strategy === "cursor" && !hasReachedEnd) {
714
- const loadedItemsCount = items.filter(
715
- (item) => item !== undefined
716
- ).length;
717
- const marginItems =
718
- rangeSize *
719
- VIEWPORT_CONSTANTS.PAGINATION.CURSOR_SCROLL_MARGIN_MULTIPLIER;
720
- const minVirtualItems =
721
- rangeSize *
722
- VIEWPORT_CONSTANTS.PAGINATION.CURSOR_MIN_VIRTUAL_SIZE_MULTIPLIER;
723
- const dynamicTotal = Math.max(
724
- loadedItemsCount + marginItems,
725
- minVirtualItems
726
- );
727
-
728
- if (dynamicTotal !== totalItems) {
729
- console.log(
730
- `[Collection] Updating cursor virtual size from ${totalItems} to ${dynamicTotal}`
731
- );
732
- setTotalItems(dynamicTotal);
733
- }
734
- }
735
- });
736
-
737
- // Listen for velocity changes
738
- component.on?.("viewport:velocity-changed", (data: any) => {
739
- const previousVelocity = currentVelocity;
740
- currentVelocity = Math.abs(data.velocity || 0);
741
-
742
- // When velocity drops below threshold, process queue
743
- if (
744
- previousVelocity > cancelLoadThreshold &&
745
- currentVelocity <= cancelLoadThreshold
746
- ) {
747
- processQueue();
748
- }
749
- });
750
-
751
- // Listen for idle state to process queue
752
- component.on?.("viewport:idle", async (data: any) => {
753
- //console.log("[Collection] Idle event received, velocity=0");
754
- currentVelocity = 0;
755
-
756
- // Reset dragging state on idle since user has stopped moving
757
- if (isDragging) {
758
- // console.log("[Collection] Resetting drag state on idle");
759
- isDragging = false;
760
- }
761
-
762
- // Get current visible range from viewport
763
- const viewportState = (component.viewport as any).state;
764
- const visibleRange = viewportState?.visibleRange;
765
-
766
- if (visibleRange) {
767
- // console.log(
768
- // `[Collection] Loading visible range on idle: ${visibleRange.start}-${visibleRange.end}`
769
- // );
770
-
771
- // Clear stale requests from queue that are far from current visible range
772
- const buffer = rangeSize * 2; // Allow some buffer
773
- loadRequestQueue = loadRequestQueue.filter((request) => {
774
- const requestEnd = request.range.end;
775
- const requestStart = request.range.start;
776
- const isRelevant =
777
- requestEnd >= visibleRange.start - buffer &&
778
- requestStart <= visibleRange.end + buffer;
779
-
780
- if (!isRelevant) {
781
- console.log(
782
- `[Collection] Removing stale queued request: ${requestStart}-${requestEnd}`
783
- );
784
- request.resolve(); // Resolve to avoid hanging promises
785
- }
786
- return isRelevant;
787
- });
788
-
789
- // Load the current visible range if needed
790
- await loadMissingRanges(visibleRange, "viewport:idle");
791
- }
792
-
793
- // Also process any queued requests
794
- processQueue();
795
- });
796
-
797
- // Load initial data if collection is available
798
- if (collection) {
799
- loadRange(0, rangeSize)
800
- .then(() => {
801
- // console.log("[Collection] Initial data loaded");
802
- })
803
- .catch((error) => {
804
- console.error("[Collection] Failed to load initial data:", error);
805
- });
806
- }
807
- };
808
-
809
- // Add collection API to viewport
810
- component.viewport.collection = {
811
- loadRange: (offset: number, limit: number) => loadRange(offset, limit),
812
- loadMissingRanges: (
813
- range: { start: number; end: number },
814
- caller?: string
815
- ) => loadMissingRanges(range, caller || "viewport.collection"),
816
- getLoadedRanges: () => loadedRanges,
817
- getPendingRanges: () => pendingRanges,
818
- clearFailedRanges: () => failedRanges.clear(),
819
- retryFailedRange,
820
- setTotalItems,
821
- getTotalItems: () => totalItems,
822
- // Cursor-specific methods
823
- getCurrentCursor: () => currentCursor,
824
- getCursorForPage: (page: number) => cursorMap.get(page) || null,
825
- };
826
-
827
- // Add collection data access with direct assignment
828
- (component as any).collection = {
829
- items,
830
- getItems: () => items,
831
- getItem: (index: number) => items[index],
832
- loadRange,
833
- loadMissingRanges: (
834
- range: { start: number; end: number },
835
- caller?: string
836
- ) => loadMissingRanges(range, caller || "component.collection"),
837
- getLoadingStats: () => ({
838
- pendingRequests: activeLoadCount,
839
- completedRequests: completedLoads,
840
- failedRequests: failedLoads,
841
- cancelledRequests: cancelledLoads,
842
- currentVelocity,
843
- canLoad: canLoad(),
844
- queuedRequests: loadRequestQueue.length,
845
- }),
846
- // Cursor methods
847
- getCurrentCursor: () => currentCursor,
848
- getCursorForPage: (page: number) => cursorMap.get(page) || null,
849
- };
850
-
851
- // Also ensure component.items is updated
852
- component.items = items;
853
-
854
- // Cleanup function
855
- const destroy = () => {
856
- // Cancel pending requests
857
- activeRequests.clear();
858
- pendingRanges.clear();
859
- };
860
-
861
- // Return enhanced component
862
- return {
863
- ...component,
864
- collection: {
865
- loadRange,
866
- loadMissingRanges: (
867
- range: { start: number; end: number },
868
- caller?: string
869
- ) => loadMissingRanges(range, caller || "return.collection"),
870
- getLoadedRanges: () => loadedRanges,
871
- getPendingRanges: () => pendingRanges,
872
- clearFailedRanges: () => failedRanges.clear(),
873
- retryFailedRange,
874
- setTotalItems,
875
- getTotalItems: () => totalItems,
876
- // Cursor-specific methods
877
- getCurrentCursor: () => currentCursor,
878
- getCursorForPage: (page: number) => cursorMap.get(page) || null,
879
- },
880
- };
881
- };
882
- }