vlist 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -28
- package/dist/internals.js +1 -60
- package/package.json +1 -1
- package/dist/constants.js +0 -83
- package/dist/core/create.js +0 -740
- package/dist/core/dom.js +0 -47
- package/dist/core/hooks.js +0 -67
- package/dist/core/index.js +0 -13
- package/dist/core/pipeline.js +0 -307
- package/dist/core/pool.js +0 -42
- package/dist/core/scroll.js +0 -137
- package/dist/core/sizes.js +0 -6
- package/dist/core/state.js +0 -56
- package/dist/core/types.js +0 -7
- package/dist/core/velocity.js +0 -33
- package/dist/events/emitter.js +0 -60
- package/dist/events/index.js +0 -6
- package/dist/plugins/a11y/index.js +0 -1
- package/dist/plugins/a11y/plugin.js +0 -259
- package/dist/plugins/async/index.js +0 -12
- package/dist/plugins/async/manager.js +0 -568
- package/dist/plugins/async/placeholder.js +0 -154
- package/dist/plugins/async/plugin.js +0 -311
- package/dist/plugins/async/sparse.js +0 -540
- package/dist/plugins/autosize/index.js +0 -4
- package/dist/plugins/autosize/plugin.js +0 -185
- package/dist/plugins/grid/index.js +0 -5
- package/dist/plugins/grid/layout.js +0 -275
- package/dist/plugins/grid/plugin.js +0 -347
- package/dist/plugins/grid/renderer.js +0 -525
- package/dist/plugins/grid/types.js +0 -11
- package/dist/plugins/groups/async-bridge.js +0 -246
- package/dist/plugins/groups/index.js +0 -13
- package/dist/plugins/groups/layout.js +0 -294
- package/dist/plugins/groups/plugin.js +0 -571
- package/dist/plugins/groups/sticky.js +0 -255
- package/dist/plugins/groups/types.js +0 -12
- package/dist/plugins/masonry/index.js +0 -6
- package/dist/plugins/masonry/layout.js +0 -261
- package/dist/plugins/masonry/plugin.js +0 -381
- package/dist/plugins/masonry/renderer.js +0 -354
- package/dist/plugins/masonry/types.js +0 -9
- package/dist/plugins/page/index.js +0 -5
- package/dist/plugins/page/plugin.js +0 -166
- package/dist/plugins/scale/index.js +0 -4
- package/dist/plugins/scale/plugin.js +0 -507
- package/dist/plugins/scrollbar/controller.js +0 -574
- package/dist/plugins/scrollbar/index.js +0 -6
- package/dist/plugins/scrollbar/plugin.js +0 -93
- package/dist/plugins/scrollbar/scrollbar.js +0 -556
- package/dist/plugins/selection/index.js +0 -7
- package/dist/plugins/selection/plugin.js +0 -601
- package/dist/plugins/selection/state.js +0 -332
- package/dist/plugins/snapshots/index.js +0 -5
- package/dist/plugins/snapshots/plugin.js +0 -301
- package/dist/plugins/sortable/index.js +0 -6
- package/dist/plugins/sortable/plugin.js +0 -753
- package/dist/plugins/table/header.js +0 -501
- package/dist/plugins/table/index.js +0 -12
- package/dist/plugins/table/layout.js +0 -211
- package/dist/plugins/table/plugin.js +0 -391
- package/dist/plugins/table/renderer.js +0 -625
- package/dist/plugins/table/types.js +0 -12
- package/dist/plugins/transition/index.js +0 -5
- package/dist/plugins/transition/plugin.js +0 -405
- package/dist/rendering/aria.js +0 -23
- package/dist/rendering/index.js +0 -18
- package/dist/rendering/measured.js +0 -98
- package/dist/rendering/renderer.js +0 -586
- package/dist/rendering/scale.js +0 -267
- package/dist/rendering/scroll.js +0 -71
- package/dist/rendering/sizes.js +0 -193
- package/dist/rendering/sort.js +0 -65
- package/dist/rendering/viewport.js +0 -268
- package/dist/types.js +0 -5
- package/dist/utils/padding.js +0 -49
- 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
|
-
};
|