vlist 1.9.0 → 2.0.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.
Files changed (182) hide show
  1. package/README.github.md +104 -97
  2. package/README.md +46 -33
  3. package/dist/constants.d.ts +11 -6
  4. package/dist/constants.js +83 -0
  5. package/dist/core/create.d.ts +10 -0
  6. package/dist/core/create.js +740 -0
  7. package/dist/core/dom.d.ts +8 -0
  8. package/dist/core/dom.js +47 -0
  9. package/dist/core/hooks.d.ts +16 -0
  10. package/dist/core/hooks.js +67 -0
  11. package/dist/core/index.d.ts +17 -0
  12. package/dist/core/index.js +13 -0
  13. package/dist/core/pipeline.d.ts +51 -0
  14. package/dist/core/pipeline.js +307 -0
  15. package/dist/core/pool.d.ts +9 -0
  16. package/dist/core/pool.js +42 -0
  17. package/dist/core/scroll.d.ts +32 -0
  18. package/dist/core/scroll.js +137 -0
  19. package/dist/core/sizes.d.ts +8 -0
  20. package/dist/core/sizes.js +6 -0
  21. package/dist/core/state.d.ts +47 -0
  22. package/dist/core/state.js +56 -0
  23. package/dist/core/types.d.ts +187 -0
  24. package/dist/core/types.js +7 -0
  25. package/dist/{builder → core}/velocity.d.ts +1 -1
  26. package/dist/core/velocity.js +33 -0
  27. package/dist/events/emitter.js +60 -0
  28. package/dist/events/index.js +6 -0
  29. package/dist/index.d.ts +28 -19
  30. package/dist/index.js +28 -1
  31. package/dist/internals.d.ts +11 -7
  32. package/dist/internals.js +60 -1
  33. package/dist/plugins/a11y/index.d.ts +2 -0
  34. package/dist/plugins/a11y/index.js +1 -0
  35. package/dist/plugins/a11y/plugin.d.ts +13 -0
  36. package/dist/plugins/a11y/plugin.js +259 -0
  37. package/dist/{features → plugins}/async/index.d.ts +1 -1
  38. package/dist/plugins/async/index.js +12 -0
  39. package/dist/{features → plugins}/async/manager.d.ts +5 -1
  40. package/dist/plugins/async/manager.js +568 -0
  41. package/dist/plugins/async/placeholder.js +154 -0
  42. package/dist/plugins/async/plugin.d.ts +48 -0
  43. package/dist/plugins/async/plugin.js +311 -0
  44. package/dist/plugins/async/sparse.js +540 -0
  45. package/dist/plugins/autosize/index.d.ts +5 -0
  46. package/dist/plugins/autosize/index.js +4 -0
  47. package/dist/plugins/autosize/plugin.d.ts +19 -0
  48. package/dist/plugins/autosize/plugin.js +185 -0
  49. package/dist/plugins/grid/index.d.ts +7 -0
  50. package/dist/plugins/grid/index.js +5 -0
  51. package/dist/plugins/grid/layout.js +275 -0
  52. package/dist/plugins/grid/plugin.d.ts +23 -0
  53. package/dist/plugins/grid/plugin.js +347 -0
  54. package/dist/plugins/grid/renderer.js +525 -0
  55. package/dist/plugins/grid/types.js +11 -0
  56. package/dist/plugins/groups/async-bridge.js +246 -0
  57. package/dist/{features → plugins}/groups/index.d.ts +1 -1
  58. package/dist/plugins/groups/index.js +13 -0
  59. package/dist/plugins/groups/layout.js +294 -0
  60. package/dist/plugins/groups/plugin.d.ts +22 -0
  61. package/dist/plugins/groups/plugin.js +571 -0
  62. package/dist/plugins/groups/sticky.js +255 -0
  63. package/dist/plugins/groups/types.js +12 -0
  64. package/dist/plugins/masonry/index.d.ts +8 -0
  65. package/dist/plugins/masonry/index.js +6 -0
  66. package/dist/plugins/masonry/layout.js +261 -0
  67. package/dist/plugins/masonry/plugin.d.ts +32 -0
  68. package/dist/plugins/masonry/plugin.js +381 -0
  69. package/dist/plugins/masonry/renderer.js +354 -0
  70. package/dist/plugins/masonry/types.js +9 -0
  71. package/dist/plugins/page/index.d.ts +5 -0
  72. package/dist/plugins/page/index.js +5 -0
  73. package/dist/plugins/page/plugin.d.ts +21 -0
  74. package/dist/plugins/page/plugin.js +166 -0
  75. package/dist/plugins/scale/index.d.ts +5 -0
  76. package/dist/plugins/scale/index.js +4 -0
  77. package/dist/plugins/scale/plugin.d.ts +24 -0
  78. package/dist/plugins/scale/plugin.js +507 -0
  79. package/dist/plugins/scrollbar/controller.js +574 -0
  80. package/dist/plugins/scrollbar/index.d.ts +7 -0
  81. package/dist/plugins/scrollbar/index.js +6 -0
  82. package/dist/plugins/scrollbar/plugin.d.ts +20 -0
  83. package/dist/plugins/scrollbar/plugin.js +93 -0
  84. package/dist/plugins/scrollbar/scrollbar.js +556 -0
  85. package/dist/plugins/selection/index.d.ts +6 -0
  86. package/dist/plugins/selection/index.js +7 -0
  87. package/dist/plugins/selection/plugin.d.ts +16 -0
  88. package/dist/plugins/selection/plugin.js +601 -0
  89. package/dist/{features → plugins}/selection/state.d.ts +8 -0
  90. package/dist/plugins/selection/state.js +332 -0
  91. package/dist/plugins/snapshots/index.d.ts +5 -0
  92. package/dist/plugins/snapshots/index.js +5 -0
  93. package/dist/plugins/snapshots/plugin.d.ts +17 -0
  94. package/dist/plugins/snapshots/plugin.js +301 -0
  95. package/dist/plugins/sortable/index.d.ts +6 -0
  96. package/dist/plugins/sortable/index.js +6 -0
  97. package/dist/plugins/sortable/plugin.d.ts +34 -0
  98. package/dist/plugins/sortable/plugin.js +753 -0
  99. package/dist/plugins/table/header.js +501 -0
  100. package/dist/{features → plugins}/table/index.d.ts +1 -1
  101. package/dist/plugins/table/index.js +12 -0
  102. package/dist/plugins/table/layout.js +211 -0
  103. package/dist/plugins/table/plugin.d.ts +20 -0
  104. package/dist/plugins/table/plugin.js +391 -0
  105. package/dist/plugins/table/renderer.js +625 -0
  106. package/dist/plugins/table/types.js +12 -0
  107. package/dist/plugins/transition/index.d.ts +5 -0
  108. package/dist/plugins/transition/index.js +5 -0
  109. package/dist/plugins/transition/plugin.d.ts +22 -0
  110. package/dist/plugins/transition/plugin.js +405 -0
  111. package/dist/rendering/aria.js +23 -0
  112. package/dist/rendering/index.js +18 -0
  113. package/dist/rendering/measured.js +98 -0
  114. package/dist/rendering/renderer.js +586 -0
  115. package/dist/rendering/scale.js +267 -0
  116. package/dist/rendering/scroll.js +71 -0
  117. package/dist/rendering/sizes.js +193 -0
  118. package/dist/rendering/sort.js +65 -0
  119. package/dist/rendering/viewport.js +268 -0
  120. package/dist/size.json +1 -1
  121. package/dist/types.js +5 -0
  122. package/dist/utils/padding.d.ts +2 -4
  123. package/dist/utils/padding.js +49 -0
  124. package/dist/utils/stats.js +124 -0
  125. package/dist/vlist-grid.css +1 -1
  126. package/dist/vlist-masonry.css +1 -1
  127. package/dist/vlist-table.css +1 -1
  128. package/dist/vlist.css +1 -1
  129. package/package.json +9 -4
  130. package/dist/builder/a11y.d.ts +0 -16
  131. package/dist/builder/api.d.ts +0 -21
  132. package/dist/builder/context.d.ts +0 -36
  133. package/dist/builder/core.d.ts +0 -16
  134. package/dist/builder/data.d.ts +0 -71
  135. package/dist/builder/dom.d.ts +0 -15
  136. package/dist/builder/index.d.ts +0 -25
  137. package/dist/builder/materialize.d.ts +0 -166
  138. package/dist/builder/pool.d.ts +0 -10
  139. package/dist/builder/range.d.ts +0 -10
  140. package/dist/builder/scroll.d.ts +0 -24
  141. package/dist/builder/types.d.ts +0 -512
  142. package/dist/features/async/feature.d.ts +0 -72
  143. package/dist/features/autosize/feature.d.ts +0 -34
  144. package/dist/features/autosize/index.d.ts +0 -2
  145. package/dist/features/grid/feature.d.ts +0 -48
  146. package/dist/features/grid/index.d.ts +0 -9
  147. package/dist/features/groups/feature.d.ts +0 -75
  148. package/dist/features/masonry/feature.d.ts +0 -45
  149. package/dist/features/masonry/index.d.ts +0 -9
  150. package/dist/features/page/feature.d.ts +0 -109
  151. package/dist/features/page/index.d.ts +0 -9
  152. package/dist/features/scale/feature.d.ts +0 -42
  153. package/dist/features/scale/index.d.ts +0 -10
  154. package/dist/features/scrollbar/feature.d.ts +0 -81
  155. package/dist/features/scrollbar/index.d.ts +0 -8
  156. package/dist/features/selection/feature.d.ts +0 -91
  157. package/dist/features/selection/index.d.ts +0 -7
  158. package/dist/features/snapshots/feature.d.ts +0 -79
  159. package/dist/features/snapshots/index.d.ts +0 -9
  160. package/dist/features/sortable/feature.d.ts +0 -101
  161. package/dist/features/sortable/index.d.ts +0 -6
  162. package/dist/features/table/feature.d.ts +0 -67
  163. package/dist/features/transition/feature.d.ts +0 -30
  164. package/dist/features/transition/index.d.ts +0 -9
  165. /package/dist/{features → plugins}/async/placeholder.d.ts +0 -0
  166. /package/dist/{features → plugins}/async/sparse.d.ts +0 -0
  167. /package/dist/{features → plugins}/grid/layout.d.ts +0 -0
  168. /package/dist/{features → plugins}/grid/renderer.d.ts +0 -0
  169. /package/dist/{features → plugins}/grid/types.d.ts +0 -0
  170. /package/dist/{features → plugins}/groups/async-bridge.d.ts +0 -0
  171. /package/dist/{features → plugins}/groups/layout.d.ts +0 -0
  172. /package/dist/{features → plugins}/groups/sticky.d.ts +0 -0
  173. /package/dist/{features → plugins}/groups/types.d.ts +0 -0
  174. /package/dist/{features → plugins}/masonry/layout.d.ts +0 -0
  175. /package/dist/{features → plugins}/masonry/renderer.d.ts +0 -0
  176. /package/dist/{features → plugins}/masonry/types.d.ts +0 -0
  177. /package/dist/{features → plugins}/scrollbar/controller.d.ts +0 -0
  178. /package/dist/{features → plugins}/scrollbar/scrollbar.d.ts +0 -0
  179. /package/dist/{features → plugins}/table/header.d.ts +0 -0
  180. /package/dist/{features → plugins}/table/layout.d.ts +0 -0
  181. /package/dist/{features → plugins}/table/renderer.d.ts +0 -0
  182. /package/dist/{features → plugins}/table/types.d.ts +0 -0
@@ -0,0 +1,540 @@
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
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * vlist v2 — Autosize Plugin
3
+ */
4
+ export { autosize, type AutosizePluginConfig } from "./plugin";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,4 @@
1
+ /**
2
+ * vlist v2 — Autosize Plugin
3
+ */
4
+ export { autosize } from "./plugin";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * vlist v2 — Autosize Plugin
3
+ *
4
+ * Enables dynamic item measurement via ResizeObserver for items with
5
+ * unknown sizes. Items are rendered without an explicit main-axis size,
6
+ * measured once by ResizeObserver, then pinned to their measured size.
7
+ *
8
+ * Priority 5 — runs before grid/masonry (10) so the measured cache
9
+ * is in place before layout plugins consume it.
10
+ *
11
+ * Requires: `item.estimatedHeight` or `item.estimatedWidth` in config
12
+ */
13
+ import type { VListItem } from "../../types";
14
+ import type { VListPlugin } from "../../core/types";
15
+ export interface AutosizePluginConfig {
16
+ gap?: number;
17
+ }
18
+ export declare function autosize<T extends VListItem = VListItem>(config?: AutosizePluginConfig): VListPlugin<T>;
19
+ //# sourceMappingURL=plugin.d.ts.map