vlist 2.0.0 → 2.0.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.
Files changed (91) hide show
  1. package/README.github.md +2 -2
  2. package/README.md +2 -2
  3. package/dist/core/dom.d.ts +1 -1
  4. package/dist/core/index.d.ts +1 -1
  5. package/dist/core/pipeline.d.ts +2 -2
  6. package/dist/core/scroll.d.ts +1 -1
  7. package/dist/core/types.d.ts +7 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +1 -28
  10. package/dist/internals.js +1 -60
  11. package/dist/plugins/scrollbar/controller.d.ts +3 -3
  12. package/dist/plugins/scrollbar/scrollbar.d.ts +2 -2
  13. package/dist/rendering/renderer.d.ts +2 -2
  14. package/dist/rendering/viewport.d.ts +1 -1
  15. package/dist/size.json +1 -1
  16. package/dist/types.d.ts +1 -1
  17. package/package.json +1 -1
  18. package/dist/constants.js +0 -83
  19. package/dist/core/create.js +0 -740
  20. package/dist/core/dom.js +0 -47
  21. package/dist/core/hooks.js +0 -67
  22. package/dist/core/index.js +0 -13
  23. package/dist/core/pipeline.js +0 -307
  24. package/dist/core/pool.js +0 -42
  25. package/dist/core/scroll.js +0 -137
  26. package/dist/core/sizes.js +0 -6
  27. package/dist/core/state.js +0 -56
  28. package/dist/core/types.js +0 -7
  29. package/dist/core/velocity.js +0 -33
  30. package/dist/events/emitter.js +0 -60
  31. package/dist/events/index.js +0 -6
  32. package/dist/plugins/a11y/index.js +0 -1
  33. package/dist/plugins/a11y/plugin.js +0 -259
  34. package/dist/plugins/async/index.js +0 -12
  35. package/dist/plugins/async/manager.js +0 -568
  36. package/dist/plugins/async/placeholder.js +0 -154
  37. package/dist/plugins/async/plugin.js +0 -311
  38. package/dist/plugins/async/sparse.js +0 -540
  39. package/dist/plugins/autosize/index.js +0 -4
  40. package/dist/plugins/autosize/plugin.js +0 -185
  41. package/dist/plugins/grid/index.js +0 -5
  42. package/dist/plugins/grid/layout.js +0 -275
  43. package/dist/plugins/grid/plugin.js +0 -347
  44. package/dist/plugins/grid/renderer.js +0 -525
  45. package/dist/plugins/grid/types.js +0 -11
  46. package/dist/plugins/groups/async-bridge.js +0 -246
  47. package/dist/plugins/groups/index.js +0 -13
  48. package/dist/plugins/groups/layout.js +0 -294
  49. package/dist/plugins/groups/plugin.js +0 -571
  50. package/dist/plugins/groups/sticky.js +0 -255
  51. package/dist/plugins/groups/types.js +0 -12
  52. package/dist/plugins/masonry/index.js +0 -6
  53. package/dist/plugins/masonry/layout.js +0 -261
  54. package/dist/plugins/masonry/plugin.js +0 -381
  55. package/dist/plugins/masonry/renderer.js +0 -354
  56. package/dist/plugins/masonry/types.js +0 -9
  57. package/dist/plugins/page/index.js +0 -5
  58. package/dist/plugins/page/plugin.js +0 -166
  59. package/dist/plugins/scale/index.js +0 -4
  60. package/dist/plugins/scale/plugin.js +0 -507
  61. package/dist/plugins/scrollbar/controller.js +0 -574
  62. package/dist/plugins/scrollbar/index.js +0 -6
  63. package/dist/plugins/scrollbar/plugin.js +0 -93
  64. package/dist/plugins/scrollbar/scrollbar.js +0 -556
  65. package/dist/plugins/selection/index.js +0 -7
  66. package/dist/plugins/selection/plugin.js +0 -601
  67. package/dist/plugins/selection/state.js +0 -332
  68. package/dist/plugins/snapshots/index.js +0 -5
  69. package/dist/plugins/snapshots/plugin.js +0 -301
  70. package/dist/plugins/sortable/index.js +0 -6
  71. package/dist/plugins/sortable/plugin.js +0 -753
  72. package/dist/plugins/table/header.js +0 -501
  73. package/dist/plugins/table/index.js +0 -12
  74. package/dist/plugins/table/layout.js +0 -211
  75. package/dist/plugins/table/plugin.js +0 -391
  76. package/dist/plugins/table/renderer.js +0 -625
  77. package/dist/plugins/table/types.js +0 -12
  78. package/dist/plugins/transition/index.js +0 -5
  79. package/dist/plugins/transition/plugin.js +0 -405
  80. package/dist/rendering/aria.js +0 -23
  81. package/dist/rendering/index.js +0 -18
  82. package/dist/rendering/measured.js +0 -98
  83. package/dist/rendering/renderer.js +0 -586
  84. package/dist/rendering/scale.js +0 -267
  85. package/dist/rendering/scroll.js +0 -71
  86. package/dist/rendering/sizes.js +0 -193
  87. package/dist/rendering/sort.js +0 -65
  88. package/dist/rendering/viewport.js +0 -268
  89. package/dist/types.js +0 -5
  90. package/dist/utils/padding.js +0 -49
  91. package/dist/utils/stats.js +0 -124
@@ -1,540 +0,0 @@
1
- /**
2
- * vlist - Sparse Storage
3
- * Efficient storage for million+ item virtual lists
4
- */
5
- // =============================================================================
6
- // Constants
7
- // =============================================================================
8
- const CHUNK_SIZE = 100;
9
- const MAX_CACHED_ITEMS = 5000;
10
- const EVICTION_BUFFER = 200;
11
- // =============================================================================
12
- // Sparse Storage Implementation
13
- // =============================================================================
14
- /**
15
- * Create sparse storage for efficient large list handling
16
- */
17
- export const createSparseStorage = (config = {}) => {
18
- const { chunkSize = CHUNK_SIZE, maxCachedItems = MAX_CACHED_ITEMS, evictionBuffer = EVICTION_BUFFER, onEvict, } = config;
19
- // Storage state
20
- const chunks = new Map();
21
- let totalItems = 0;
22
- let cachedItemCount = 0;
23
- // ==========================================================================
24
- // Internal Helpers
25
- // ==========================================================================
26
- /**
27
- * Get or create a chunk
28
- */
29
- const getOrCreateChunk = (chunkIndex) => {
30
- let chunk = chunks.get(chunkIndex);
31
- if (!chunk) {
32
- chunk = {
33
- items: new Array(chunkSize),
34
- count: 0,
35
- lastAccess: Date.now(),
36
- };
37
- chunks.set(chunkIndex, chunk);
38
- }
39
- else {
40
- chunk.lastAccess = Date.now();
41
- }
42
- return chunk;
43
- };
44
- /**
45
- * Get chunk index for item index
46
- */
47
- const getChunkIndex = (itemIndex) => {
48
- return Math.floor(itemIndex / chunkSize);
49
- };
50
- /**
51
- * Get index within chunk
52
- */
53
- const getIndexInChunk = (itemIndex) => {
54
- return itemIndex % chunkSize;
55
- };
56
- // ==========================================================================
57
- // Total Management
58
- // ==========================================================================
59
- const getTotal = () => totalItems;
60
- const setTotal = (total) => {
61
- totalItems = total;
62
- };
63
- // ==========================================================================
64
- // Item Access
65
- // ==========================================================================
66
- const get = (index) => {
67
- if (index < 0 || index >= totalItems) {
68
- return undefined;
69
- }
70
- const chunkIndex = getChunkIndex(index);
71
- const chunk = chunks.get(chunkIndex);
72
- if (!chunk) {
73
- // log(`get: index=${index}, chunkIndex=${chunkIndex} - chunk not found`);
74
- return undefined;
75
- }
76
- return chunk.items[getIndexInChunk(index)];
77
- };
78
- const has = (index) => {
79
- if (index < 0 || index >= totalItems) {
80
- return false;
81
- }
82
- const chunkIndex = getChunkIndex(index);
83
- const chunk = chunks.get(chunkIndex);
84
- if (!chunk) {
85
- return false;
86
- }
87
- return chunk.items[getIndexInChunk(index)] !== undefined;
88
- };
89
- const set = (index, item) => {
90
- const chunkIndex = getChunkIndex(index);
91
- const chunk = getOrCreateChunk(chunkIndex);
92
- const indexInChunk = getIndexInChunk(index);
93
- // Track if this is a new item
94
- const isNew = chunk.items[indexInChunk] === undefined;
95
- chunk.items[indexInChunk] = item;
96
- if (isNew) {
97
- chunk.count++;
98
- cachedItemCount++;
99
- // log(
100
- // `set: index=${index}, chunkIndex=${chunkIndex}, cachedItemCount=${cachedItemCount}`,
101
- // );
102
- }
103
- // Update total if needed
104
- if (index >= totalItems) {
105
- totalItems = index + 1;
106
- }
107
- };
108
- const setRange = (offset, items) => {
109
- for (let i = 0; i < items.length; i++) {
110
- const item = items[i];
111
- if (item !== undefined) {
112
- set(offset + i, item);
113
- }
114
- }
115
- };
116
- const insertItem = (index, item) => {
117
- if (index < 0 || index > totalItems)
118
- return;
119
- // Collect all loaded items at indices >= insertIndex, in order.
120
- const insertChunkIdx = getChunkIndex(index);
121
- const sortedChunkKeys = Array.from(chunks.keys())
122
- .filter(k => k >= insertChunkIdx)
123
- .sort((a, b) => a - b);
124
- const shifted = [];
125
- for (const ci of sortedChunkKeys) {
126
- const c = chunks.get(ci);
127
- const base = ci * chunkSize;
128
- for (let s = 0; s < chunkSize; s++) {
129
- if (c.items[s] === undefined)
130
- continue;
131
- const itemIndex = base + s;
132
- if (itemIndex < index)
133
- continue;
134
- shifted.push({ oldIndex: itemIndex, item: c.items[s] });
135
- }
136
- }
137
- // 1. Clear items that will shift up
138
- for (const { oldIndex } of shifted) {
139
- const ci = getChunkIndex(oldIndex);
140
- const c = chunks.get(ci);
141
- if (!c)
142
- continue;
143
- const slot = getIndexInChunk(oldIndex);
144
- if (c.items[slot] !== undefined) {
145
- c.items[slot] = undefined;
146
- c.count--;
147
- cachedItemCount--;
148
- if (c.count === 0)
149
- chunks.delete(ci);
150
- }
151
- }
152
- // 2. Increase total first (so getChunkIndex works for new positions)
153
- totalItems++;
154
- // 3. Re-insert each shifted item at (oldIndex + 1)
155
- for (const { oldIndex, item: shiftedItem } of shifted) {
156
- const newIndex = oldIndex + 1;
157
- const ci = getChunkIndex(newIndex);
158
- const dst = getOrCreateChunk(ci);
159
- const slot = getIndexInChunk(newIndex);
160
- dst.items[slot] = shiftedItem;
161
- dst.count++;
162
- cachedItemCount++;
163
- }
164
- // 4. Insert the new item
165
- const ci = getChunkIndex(index);
166
- const dst = getOrCreateChunk(ci);
167
- const slot = getIndexInChunk(index);
168
- dst.items[slot] = item;
169
- dst.count++;
170
- cachedItemCount++;
171
- };
172
- const deleteItem = (index) => {
173
- if (index < 0 || index >= totalItems)
174
- return false;
175
- // Check if the item at this index is loaded
176
- const chunkIdx = getChunkIndex(index);
177
- const chunk = chunks.get(chunkIdx);
178
- const slotInChunk = getIndexInChunk(index);
179
- const wasLoaded = chunk !== undefined &&
180
- chunk.items[slotInChunk] !== undefined;
181
- if (!wasLoaded)
182
- return false;
183
- // Collect all loaded items at indices > deleted index, in order.
184
- // This is O(cachedItems) instead of O(totalItems) — critical for
185
- // lists with 300K+ items where only ~50 are in memory.
186
- const deletedChunkIdx = chunkIdx;
187
- const sortedChunkKeys = Array.from(chunks.keys())
188
- .filter(k => k >= deletedChunkIdx)
189
- .sort((a, b) => a - b);
190
- const shifted = [];
191
- for (const ci of sortedChunkKeys) {
192
- const c = chunks.get(ci);
193
- const base = ci * chunkSize;
194
- for (let s = 0; s < chunkSize; s++) {
195
- if (c.items[s] === undefined)
196
- continue;
197
- const itemIndex = base + s;
198
- if (itemIndex <= index)
199
- continue; // before or at deleted — skip
200
- shifted.push({ oldIndex: itemIndex, item: c.items[s] });
201
- }
202
- }
203
- // 1. Remove the deleted item and all items that will shift
204
- // Clear the deleted item
205
- chunk.items[slotInChunk] = undefined;
206
- chunk.count--;
207
- cachedItemCount--;
208
- if (chunk.count === 0)
209
- chunks.delete(chunkIdx);
210
- // Clear each item that will be re-inserted at a lower index
211
- for (const { oldIndex } of shifted) {
212
- const ci = getChunkIndex(oldIndex);
213
- const c = chunks.get(ci);
214
- if (!c)
215
- continue;
216
- const slot = getIndexInChunk(oldIndex);
217
- if (c.items[slot] !== undefined) {
218
- c.items[slot] = undefined;
219
- c.count--;
220
- cachedItemCount--;
221
- if (c.count === 0)
222
- chunks.delete(ci);
223
- }
224
- }
225
- // 2. Re-insert each shifted item at (oldIndex - 1)
226
- for (const { oldIndex, item } of shifted) {
227
- const newIndex = oldIndex - 1;
228
- const ci = getChunkIndex(newIndex);
229
- const dst = getOrCreateChunk(ci);
230
- const slot = getIndexInChunk(newIndex);
231
- dst.items[slot] = item;
232
- dst.count++;
233
- cachedItemCount++;
234
- }
235
- // 3. Decrease total
236
- totalItems--;
237
- return true;
238
- };
239
- // ==========================================================================
240
- // Range Operations
241
- // ==========================================================================
242
- const getRange = (start, end) => {
243
- const result = [];
244
- for (let i = start; i <= end && i < totalItems; i++) {
245
- result.push(get(i));
246
- }
247
- return result;
248
- };
249
- const isRangeLoaded = (start, end) => {
250
- for (let i = start; i <= end && i < totalItems; i++) {
251
- if (!has(i)) {
252
- return false;
253
- }
254
- }
255
- return true;
256
- };
257
- const getLoadedRanges = () => {
258
- const ranges = [];
259
- let currentRange = null;
260
- // Iterate through all chunks in order
261
- const sortedChunkIndices = Array.from(chunks.keys()).sort((a, b) => a - b);
262
- for (const chunkIndex of sortedChunkIndices) {
263
- const chunk = chunks.get(chunkIndex);
264
- if (!chunk)
265
- continue;
266
- const chunkStart = chunkIndex * chunkSize;
267
- // Find loaded items in this chunk
268
- for (let i = 0; i < chunkSize; i++) {
269
- const itemIndex = chunkStart + i;
270
- if (itemIndex >= totalItems)
271
- break;
272
- if (chunk.items[i] !== undefined) {
273
- if (currentRange === null) {
274
- currentRange = { start: itemIndex, end: itemIndex };
275
- }
276
- else if (itemIndex === currentRange.end + 1) {
277
- currentRange.end = itemIndex;
278
- }
279
- else {
280
- ranges.push(currentRange);
281
- currentRange = { start: itemIndex, end: itemIndex };
282
- }
283
- }
284
- else if (currentRange !== null) {
285
- ranges.push(currentRange);
286
- currentRange = null;
287
- }
288
- }
289
- }
290
- if (currentRange !== null) {
291
- ranges.push(currentRange);
292
- }
293
- return ranges;
294
- };
295
- const findUnloadedRanges = (start, end) => {
296
- const unloaded = [];
297
- let currentRange = null;
298
- for (let i = start; i <= end && i < totalItems; i++) {
299
- if (!has(i)) {
300
- if (currentRange === null) {
301
- currentRange = { start: i, end: i };
302
- }
303
- else {
304
- currentRange.end = i;
305
- }
306
- }
307
- else if (currentRange !== null) {
308
- unloaded.push(currentRange);
309
- currentRange = null;
310
- }
311
- }
312
- if (currentRange !== null) {
313
- unloaded.push(currentRange);
314
- }
315
- return unloaded;
316
- };
317
- // ==========================================================================
318
- // Chunk Operations
319
- // ==========================================================================
320
- const isChunkLoaded = (chunkIndex) => {
321
- return chunks.has(chunkIndex);
322
- };
323
- const isChunkFullyLoaded = (chunkIndex) => {
324
- const chunk = chunks.get(chunkIndex);
325
- if (!chunk)
326
- return false;
327
- const chunkStart = chunkIndex * chunkSize;
328
- const expectedCount = Math.min(chunkSize, totalItems - chunkStart);
329
- return chunk.count >= expectedCount;
330
- };
331
- const touchChunk = (chunkIndex) => {
332
- const chunk = chunks.get(chunkIndex);
333
- if (chunk) {
334
- chunk.lastAccess = Date.now();
335
- }
336
- };
337
- /**
338
- * Mark all chunks covering a range as accessed with a single timestamp.
339
- * Batches Date.now() to one call instead of per-item in get().
340
- */
341
- const touchChunksForRange = (start, end) => {
342
- if (start > end || chunks.size === 0)
343
- return;
344
- const now = Date.now();
345
- const startChunk = getChunkIndex(Math.max(0, start));
346
- const endChunk = getChunkIndex(Math.min(totalItems - 1, end));
347
- for (let ci = startChunk; ci <= endChunk; ci++) {
348
- const chunk = chunks.get(ci);
349
- if (chunk) {
350
- chunk.lastAccess = now;
351
- }
352
- }
353
- };
354
- // ==========================================================================
355
- // Eviction
356
- // ==========================================================================
357
- /**
358
- * Evict chunks far from visible range
359
- */
360
- const evictDistant = (visibleStart, visibleEnd) => {
361
- // Only evict if we exceed the limit
362
- if (cachedItemCount <= maxCachedItems) {
363
- return 0;
364
- }
365
- // Calculate keep zone with buffer
366
- const keepStart = Math.max(0, visibleStart - evictionBuffer);
367
- const keepEnd = Math.min(totalItems - 1, visibleEnd + evictionBuffer);
368
- const keepChunkStart = getChunkIndex(keepStart);
369
- const keepChunkEnd = getChunkIndex(keepEnd);
370
- let evictedCount = 0;
371
- const evictedRanges = [];
372
- // Find chunks to evict
373
- for (const [chunkIndex, chunk] of chunks) {
374
- if (chunkIndex < keepChunkStart || chunkIndex > keepChunkEnd) {
375
- evictedCount += chunk.count;
376
- evictedRanges.push(chunkIndex);
377
- cachedItemCount -= chunk.count;
378
- chunks.delete(chunkIndex);
379
- }
380
- }
381
- // Notify about eviction
382
- if (evictedCount > 0 && onEvict) {
383
- onEvict(evictedCount, evictedRanges);
384
- }
385
- return evictedCount;
386
- };
387
- /**
388
- * Force eviction using LRU to meet memory limit
389
- */
390
- const evictToLimit = () => {
391
- if (cachedItemCount <= maxCachedItems) {
392
- return 0;
393
- }
394
- // Sort chunks by last access (oldest first)
395
- const sortedChunks = Array.from(chunks.entries()).sort(([, a], [, b]) => a.lastAccess - b.lastAccess);
396
- let evictedCount = 0;
397
- const evictedRanges = [];
398
- // Evict oldest chunks until under limit
399
- for (const [chunkIndex, chunk] of sortedChunks) {
400
- if (cachedItemCount <= maxCachedItems) {
401
- break;
402
- }
403
- evictedCount += chunk.count;
404
- cachedItemCount -= chunk.count;
405
- evictedRanges.push(chunkIndex);
406
- chunks.delete(chunkIndex);
407
- }
408
- // Notify about eviction
409
- if (evictedCount > 0 && onEvict) {
410
- onEvict(evictedCount, evictedRanges);
411
- }
412
- return evictedCount;
413
- };
414
- // ==========================================================================
415
- // Statistics
416
- // ==========================================================================
417
- const getStats = () => {
418
- return {
419
- totalItems,
420
- cachedItems: cachedItemCount,
421
- cachedChunks: chunks.size,
422
- chunkSize,
423
- maxCachedItems,
424
- memoryEfficiency: totalItems > 0 ? 1 - cachedItemCount / totalItems : 1,
425
- };
426
- };
427
- const getCachedCount = () => cachedItemCount;
428
- // ==========================================================================
429
- // Lifecycle
430
- // ==========================================================================
431
- const clear = () => {
432
- chunks.clear();
433
- cachedItemCount = 0;
434
- };
435
- const reset = () => {
436
- clear();
437
- totalItems = 0;
438
- };
439
- // ==========================================================================
440
- // Return Public API
441
- // ==========================================================================
442
- return {
443
- chunkSize,
444
- maxCachedItems,
445
- getTotal,
446
- setTotal,
447
- get,
448
- has,
449
- set,
450
- setRange,
451
- insert: insertItem,
452
- delete: deleteItem,
453
- getRange,
454
- isRangeLoaded,
455
- getLoadedRanges,
456
- findUnloadedRanges,
457
- getChunkIndex,
458
- isChunkLoaded,
459
- isChunkFullyLoaded,
460
- touchChunk,
461
- touchChunksForRange,
462
- evictDistant,
463
- evictToLimit,
464
- getStats,
465
- getCachedCount,
466
- clear,
467
- reset,
468
- };
469
- };
470
- // =============================================================================
471
- // Utility Functions
472
- // =============================================================================
473
- /**
474
- * Merge adjacent/overlapping ranges
475
- */
476
- export const mergeRanges = (ranges) => {
477
- if (ranges.length === 0)
478
- return [];
479
- // Sort by start
480
- const sorted = [...ranges].sort((a, b) => a.start - b.start);
481
- const merged = [{ ...sorted[0] }];
482
- for (let i = 1; i < sorted.length; i++) {
483
- const current = sorted[i];
484
- const last = merged[merged.length - 1];
485
- if (current.start <= last.end + 1) {
486
- // Overlapping or adjacent - merge
487
- last.end = Math.max(last.end, current.end);
488
- }
489
- else {
490
- // Gap - new range
491
- merged.push({ ...current });
492
- }
493
- }
494
- return merged;
495
- };
496
- /**
497
- * Calculate ranges that need to be loaded
498
- */
499
- export const calculateMissingRanges = (needed, loaded, chunkSize) => {
500
- // Align to chunk boundaries for efficient loading
501
- const alignedStart = Math.floor(needed.start / chunkSize) * chunkSize;
502
- const alignedEnd = Math.ceil((needed.end + 1) / chunkSize) * chunkSize - 1;
503
- const alignedNeeded = { start: alignedStart, end: alignedEnd };
504
- if (loaded.length === 0) {
505
- return [alignedNeeded];
506
- }
507
- const missing = [];
508
- const merged = mergeRanges(loaded);
509
- let current = alignedNeeded.start;
510
- for (const range of merged) {
511
- // Skip ranges that end before our current position
512
- if (range.end < current) {
513
- continue;
514
- }
515
- // If this range starts after our aligned needed range, we're done
516
- // (any remaining gap will be handled after the loop)
517
- if (range.start > alignedNeeded.end) {
518
- break;
519
- }
520
- // If there's a gap before this loaded range, record it
521
- if (range.start > current) {
522
- missing.push({
523
- start: current,
524
- end: Math.min(range.start - 1, alignedNeeded.end),
525
- });
526
- }
527
- // Advance current past this loaded range
528
- current = range.end + 1;
529
- if (current > alignedNeeded.end)
530
- break;
531
- }
532
- // Check for gap after all loaded ranges
533
- if (current <= alignedNeeded.end) {
534
- missing.push({
535
- start: current,
536
- end: alignedNeeded.end,
537
- });
538
- }
539
- return missing;
540
- };
@@ -1,4 +0,0 @@
1
- /**
2
- * vlist v2 — Autosize Plugin
3
- */
4
- export { autosize } from "./plugin";