@humanspeak/svelte-virtual-list 0.2.4 → 0.2.6-beta.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.
@@ -1,4 +1,4 @@
1
- import type { SvelteVirtualListMode } from '../types.js';
1
+ import type { SvelteVirtualListMode, SvelteVirtualListPreviousVisibleRange } from '../types.js';
2
2
  import type { VirtualListSetters, VirtualListState } from './types.js';
3
3
  /**
4
4
  * Calculates the maximum scroll position for a virtual list.
@@ -26,12 +26,9 @@ export declare const calculateScrollPosition: (totalItems: number, itemHeight: n
26
26
  * @param {number} totalItems - Total number of items in the list
27
27
  * @param {number} bufferSize - Number of items to render outside the visible area
28
28
  * @param {SvelteVirtualListMode} mode - Scroll direction mode
29
- * @returns {{ start: number, end: number }} Range of indices to render
29
+ * @returns {SvelteVirtualListPreviousVisibleRange} Range of indices to render
30
30
  */
31
- export declare const calculateVisibleRange: (scrollTop: number, viewportHeight: number, itemHeight: number, totalItems: number, bufferSize: number, mode: SvelteVirtualListMode) => {
32
- start: number;
33
- end: number;
34
- };
31
+ export declare const calculateVisibleRange: (scrollTop: number, viewportHeight: number, itemHeight: number, totalItems: number, bufferSize: number, mode: SvelteVirtualListMode) => SvelteVirtualListPreviousVisibleRange;
35
32
  /**
36
33
  * Calculates the CSS transform value for positioning the virtual list items.
37
34
  *
@@ -64,20 +61,19 @@ export declare const updateHeightAndScroll: (state: VirtualListState, setters: V
64
61
  * Calculates the average height of visible items in a virtual list.
65
62
  *
66
63
  * This function optimizes performance by:
67
- * 1. Using a height cache to store measured item heights
64
+ * 1. Using a height cache to store measured item heights with dirty tracking
68
65
  * 2. Only measuring new items not in the cache
69
66
  * 3. Calculating a running average of all measured heights
70
67
  *
71
68
  * @param {HTMLElement[]} itemElements - Array of currently rendered item elements
72
69
  * @param {{ start: number }} visibleRange - Object containing the start index of visible items
73
- * @param {Record<number, number>} heightCache - Cache of previously measured item heights
74
- * @param {number} lastMeasuredIndex - Index of the last measured item
70
+ * @param {HeightCache} heightCache - Cache of previously measured item heights with dirty tracking
75
71
  * @param {number} currentItemHeight - Current average item height being used
76
72
  *
77
73
  * @returns {{
78
74
  * newHeight: number,
79
75
  * newLastMeasuredIndex: number,
80
- * updatedHeightCache: Record<number, number>
76
+ * updatedHeightCache: HeightCache
81
77
  * }} Object containing new calculated height, last measured index, and updated cache
82
78
  *
83
79
  * @example
@@ -85,13 +81,12 @@ export declare const updateHeightAndScroll: (state: VirtualListState, setters: V
85
81
  * itemElements,
86
82
  * { start: 0 },
87
83
  * {},
88
- * -1,
89
84
  * 40
90
85
  * )
91
86
  */
92
87
  export declare const calculateAverageHeight: (itemElements: HTMLElement[], visibleRange: {
93
88
  start: number;
94
- }, heightCache: Record<number, number>, lastMeasuredIndex: number, currentItemHeight: number) => {
89
+ }, heightCache: Record<number, number>, currentItemHeight: number) => {
95
90
  newHeight: number;
96
91
  newLastMeasuredIndex: number;
97
92
  updatedHeightCache: Record<number, number>;
@@ -120,4 +115,39 @@ export declare const calculateAverageHeight: (itemElements: HTMLElement[], visib
120
115
  * () => console.log('All items processed')
121
116
  * )
122
117
  */
123
- export declare const processChunked: (items: any[], chunkSize: number, onProgress: (processed: number) => void, onComplete: () => void) => Promise<void>;
118
+ export declare const processChunked: (items: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
119
+ chunkSize: number, onProgress: (processed: number) => void, // eslint-disable-line no-unused-vars
120
+ onComplete: () => void) => Promise<void>;
121
+ /**
122
+ * Builds a block sum array for fast offset calculation in large virtual lists.
123
+ * Each entry in the array is the total height up to the end of that block (exclusive).
124
+ *
125
+ * @param {HeightCache} heightCache - Map of measured item heights with dirty tracking
126
+ * @param {number} calculatedItemHeight - Estimated height for unmeasured items
127
+ * @param {number} totalItems - Total number of items in the list
128
+ * @param {number} blockSize - Number of items per block
129
+ * @returns {number[]} Array of prefix sums at each block boundary
130
+ */
131
+ export declare const buildBlockSums: (heightCache: Record<number, number>, calculatedItemHeight: number, totalItems: number, blockSize?: number) => number[];
132
+ /**
133
+ * Calculates the scroll offset (in pixels) needed to bring a specific item into view in a virtual list.
134
+ *
135
+ * Uses block memoization for efficient O(b) offset calculation, where b = block size (default 1000).
136
+ * For very large lists, this avoids O(n) iteration for every scroll.
137
+ *
138
+ * - For indices >= blockSize, sums the block prefix, then only iterates the tail within the block.
139
+ * - For small indices, falls back to the original logic.
140
+ *
141
+ * @param {HeightCache} heightCache - Map of measured item heights with dirty tracking
142
+ * @param {number} calculatedItemHeight - Estimated height for unmeasured items
143
+ * @param {number} idx - The index to scroll to (exclusive)
144
+ * @param {number[]} [blockSums] - Optional precomputed block sums (for repeated queries)
145
+ * @param {number} [blockSize=1000] - Block size for memoization
146
+ * @returns {number} The total offset in pixels from the top of the list to the start of the item at idx.
147
+ *
148
+ * @example
149
+ * // For best performance with repeated queries:
150
+ * const blockSums = buildBlockSums(heightCache, calculatedItemHeight, items.length);
151
+ * const offset = getScrollOffsetForIndex(heightCache, calculatedItemHeight, 12345, blockSums);
152
+ */
153
+ export declare const getScrollOffsetForIndex: (heightCache: Record<number, number>, calculatedItemHeight: number, idx: number, blockSums?: number[], blockSize?: number) => number;
@@ -29,7 +29,7 @@ export const calculateScrollPosition = (totalItems, itemHeight, containerHeight)
29
29
  * @param {number} totalItems - Total number of items in the list
30
30
  * @param {number} bufferSize - Number of items to render outside the visible area
31
31
  * @param {SvelteVirtualListMode} mode - Scroll direction mode
32
- * @returns {{ start: number, end: number }} Range of indices to render
32
+ * @returns {SvelteVirtualListPreviousVisibleRange} Range of indices to render
33
33
  */
34
34
  export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, totalItems, bufferSize, mode) => {
35
35
  if (mode === 'bottomToTop') {
@@ -101,20 +101,19 @@ export const updateHeightAndScroll = (state, setters, immediate = false) => {
101
101
  * Calculates the average height of visible items in a virtual list.
102
102
  *
103
103
  * This function optimizes performance by:
104
- * 1. Using a height cache to store measured item heights
104
+ * 1. Using a height cache to store measured item heights with dirty tracking
105
105
  * 2. Only measuring new items not in the cache
106
106
  * 3. Calculating a running average of all measured heights
107
107
  *
108
108
  * @param {HTMLElement[]} itemElements - Array of currently rendered item elements
109
109
  * @param {{ start: number }} visibleRange - Object containing the start index of visible items
110
- * @param {Record<number, number>} heightCache - Cache of previously measured item heights
111
- * @param {number} lastMeasuredIndex - Index of the last measured item
110
+ * @param {HeightCache} heightCache - Cache of previously measured item heights with dirty tracking
112
111
  * @param {number} currentItemHeight - Current average item height being used
113
112
  *
114
113
  * @returns {{
115
114
  * newHeight: number,
116
115
  * newLastMeasuredIndex: number,
117
- * updatedHeightCache: Record<number, number>
116
+ * updatedHeightCache: HeightCache
118
117
  * }} Object containing new calculated height, last measured index, and updated cache
119
118
  *
120
119
  * @example
@@ -122,16 +121,15 @@ export const updateHeightAndScroll = (state, setters, immediate = false) => {
122
121
  * itemElements,
123
122
  * { start: 0 },
124
123
  * {},
125
- * -1,
126
124
  * 40
127
125
  * )
128
126
  */
129
- export const calculateAverageHeight = (itemElements, visibleRange, heightCache, lastMeasuredIndex, currentItemHeight) => {
127
+ export const calculateAverageHeight = (itemElements, visibleRange, heightCache, currentItemHeight) => {
130
128
  const validElements = itemElements.filter((el) => el);
131
129
  if (validElements.length === 0) {
132
130
  return {
133
131
  newHeight: currentItemHeight,
134
- newLastMeasuredIndex: lastMeasuredIndex,
132
+ newLastMeasuredIndex: visibleRange.start,
135
133
  updatedHeightCache: heightCache
136
134
  };
137
135
  }
@@ -140,14 +138,23 @@ export const calculateAverageHeight = (itemElements, visibleRange, heightCache,
140
138
  validElements.forEach((el, i) => {
141
139
  const itemIndex = visibleRange.start + i;
142
140
  if (!newHeightCache[itemIndex]) {
143
- newHeightCache[itemIndex] = el.getBoundingClientRect().height;
141
+ try {
142
+ const height = el.getBoundingClientRect().height;
143
+ if (Number.isFinite(height) && height > 0) {
144
+ newHeightCache[itemIndex] = height;
145
+ }
146
+ }
147
+ catch {
148
+ // Skip invalid measurements
149
+ }
144
150
  }
145
151
  });
146
- // Calculate average from cached heights
147
- const heights = Object.values(newHeightCache);
148
- const averageHeight = heights.reduce((sum, h) => sum + h, 0) / heights.length;
152
+ // Calculate average from valid cached heights
153
+ const validHeights = Object.values(newHeightCache).filter((h) => Number.isFinite(h) && h > 0);
149
154
  return {
150
- newHeight: averageHeight > 0 && !isNaN(averageHeight) ? averageHeight : currentItemHeight,
155
+ newHeight: validHeights.length > 0
156
+ ? validHeights.reduce((sum, h) => sum + h, 0) / validHeights.length
157
+ : currentItemHeight,
151
158
  newLastMeasuredIndex: visibleRange.start,
152
159
  updatedHeightCache: newHeightCache
153
160
  };
@@ -195,3 +202,68 @@ onComplete) => {
195
202
  };
196
203
  await processChunk(0);
197
204
  };
205
+ /**
206
+ * Builds a block sum array for fast offset calculation in large virtual lists.
207
+ * Each entry in the array is the total height up to the end of that block (exclusive).
208
+ *
209
+ * @param {HeightCache} heightCache - Map of measured item heights with dirty tracking
210
+ * @param {number} calculatedItemHeight - Estimated height for unmeasured items
211
+ * @param {number} totalItems - Total number of items in the list
212
+ * @param {number} blockSize - Number of items per block
213
+ * @returns {number[]} Array of prefix sums at each block boundary
214
+ */
215
+ export const buildBlockSums = (heightCache, calculatedItemHeight, totalItems, blockSize = 1000) => {
216
+ const blockSums = [];
217
+ let sum = 0;
218
+ for (let i = 0; i < totalItems; i++) {
219
+ sum += heightCache[i] ?? calculatedItemHeight;
220
+ if ((i + 1) % blockSize === 0) {
221
+ blockSums.push(sum);
222
+ }
223
+ }
224
+ // Push the last partial block if needed
225
+ if (totalItems % blockSize !== 0) {
226
+ blockSums.push(sum);
227
+ }
228
+ return blockSums;
229
+ };
230
+ /**
231
+ * Calculates the scroll offset (in pixels) needed to bring a specific item into view in a virtual list.
232
+ *
233
+ * Uses block memoization for efficient O(b) offset calculation, where b = block size (default 1000).
234
+ * For very large lists, this avoids O(n) iteration for every scroll.
235
+ *
236
+ * - For indices >= blockSize, sums the block prefix, then only iterates the tail within the block.
237
+ * - For small indices, falls back to the original logic.
238
+ *
239
+ * @param {HeightCache} heightCache - Map of measured item heights with dirty tracking
240
+ * @param {number} calculatedItemHeight - Estimated height for unmeasured items
241
+ * @param {number} idx - The index to scroll to (exclusive)
242
+ * @param {number[]} [blockSums] - Optional precomputed block sums (for repeated queries)
243
+ * @param {number} [blockSize=1000] - Block size for memoization
244
+ * @returns {number} The total offset in pixels from the top of the list to the start of the item at idx.
245
+ *
246
+ * @example
247
+ * // For best performance with repeated queries:
248
+ * const blockSums = buildBlockSums(heightCache, calculatedItemHeight, items.length);
249
+ * const offset = getScrollOffsetForIndex(heightCache, calculatedItemHeight, 12345, blockSums);
250
+ */
251
+ export const getScrollOffsetForIndex = (heightCache, calculatedItemHeight, idx, blockSums, blockSize = 1000) => {
252
+ if (idx <= 0)
253
+ return 0;
254
+ if (!blockSums) {
255
+ // Fallback: O(n) for a single query
256
+ let offset = 0;
257
+ for (let i = 0; i < idx; i++) {
258
+ offset += heightCache[i] ?? calculatedItemHeight;
259
+ }
260
+ return offset;
261
+ }
262
+ const blockIdx = Math.floor(idx / blockSize);
263
+ let offset = blockIdx > 0 ? blockSums[blockIdx - 1] : 0;
264
+ const start = blockIdx * blockSize;
265
+ for (let i = start; i < idx; i++) {
266
+ offset += heightCache[i] ?? calculatedItemHeight;
267
+ }
268
+ return offset;
269
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-virtual-list",
3
- "version": "0.2.4",
3
+ "version": "0.2.6-beta.0",
4
4
  "description": "A lightweight, high-performance virtual list component for Svelte 5 that renders large datasets with minimal memory usage. Features include dynamic height support, smooth scrolling, TypeScript support, and efficient DOM recycling. Ideal for infinite scrolling lists, data tables, chat interfaces, and any application requiring the rendering of thousands of items without compromising performance. Zero dependencies and fully customizable.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -43,7 +43,8 @@
43
43
  "files": [
44
44
  "dist",
45
45
  "!dist/**/*.test.*",
46
- "!dist/**/*.spec.*"
46
+ "!dist/**/*.spec.*",
47
+ "!dist/test/**/*"
47
48
  ],
48
49
  "scripts": {
49
50
  "build": "vite build && npm run package",
@@ -65,52 +66,60 @@
65
66
  "test:only": "vitest run",
66
67
  "test:watch": "vitest"
67
68
  },
69
+ "overrides": {
70
+ "@sveltejs/kit": {
71
+ "cookie": "^0.7.0"
72
+ }
73
+ },
68
74
  "dependencies": {
69
- "esm-env": "^1.2.2"
75
+ "esm-env": "^1.2.2",
76
+ "runed": "^0.31.1"
70
77
  },
71
78
  "devDependencies": {
72
- "@eslint/js": "^9.18.0",
73
- "@playwright/test": "^1.49.1",
74
- "@sveltejs/adapter-auto": "^4.0.0",
75
- "@sveltejs/kit": "^2.16.1",
76
- "@sveltejs/package": "^2.3.8",
77
- "@sveltejs/vite-plugin-svelte": "^5.0.3",
78
- "@testing-library/jest-dom": "^6.6.3",
79
- "@testing-library/svelte": "^5.2.6",
80
- "@testing-library/user-event": "^14.6.0",
81
- "@types/node": "^22.10.7",
82
- "@typescript-eslint/eslint-plugin": "^8.21.0",
83
- "@typescript-eslint/parser": "^8.21.0",
84
- "@vitest/coverage-v8": "^3.0.3",
85
- "eslint": "^9.18.0",
86
- "eslint-config-prettier": "^10.0.1",
87
- "eslint-plugin-svelte": "^2.46.1",
88
- "globals": "^15.14.0",
89
- "jsdom": "^26.0.0",
90
- "prettier": "^3.4.2",
91
- "prettier-plugin-organize-imports": "^4.1.0",
92
- "prettier-plugin-svelte": "^3.3.3",
93
- "publint": "^0.3.2",
94
- "svelte": "^5.19.1",
95
- "svelte-check": "^4.1.4",
96
- "typescript": "^5.7.3",
97
- "vite": "^6.0.11",
98
- "vitest": "^3.0.3"
79
+ "@eslint/compat": "^1.3.1",
80
+ "@eslint/js": "^9.32.0",
81
+ "@faker-js/faker": "^9.9.0",
82
+ "@playwright/test": "^1.54.1",
83
+ "@sveltejs/adapter-auto": "^6.0.1",
84
+ "@sveltejs/kit": "^2.26.1",
85
+ "@sveltejs/package": "^2.4.0",
86
+ "@sveltejs/vite-plugin-svelte": "^6.1.0",
87
+ "@testing-library/jest-dom": "^6.6.4",
88
+ "@testing-library/svelte": "^5.2.8",
89
+ "@testing-library/user-event": "^14.6.1",
90
+ "@types/node": "^24.1.0",
91
+ "@typescript-eslint/eslint-plugin": "^8.38.0",
92
+ "@typescript-eslint/parser": "^8.38.0",
93
+ "@vitest/coverage-v8": "^3.2.4",
94
+ "eslint": "^9.32.0",
95
+ "eslint-config-prettier": "^10.1.8",
96
+ "eslint-plugin-import": "^2.32.0",
97
+ "eslint-plugin-svelte": "^3.11.0",
98
+ "eslint-plugin-unused-imports": "^4.1.4",
99
+ "globals": "^16.3.0",
100
+ "jsdom": "^26.1.0",
101
+ "prettier": "^3.6.2",
102
+ "prettier-plugin-organize-imports": "^4.2.0",
103
+ "prettier-plugin-sort-json": "^4.1.1",
104
+ "prettier-plugin-svelte": "^3.4.0",
105
+ "prettier-plugin-tailwindcss": "^0.6.14",
106
+ "publint": "^0.3.12",
107
+ "svelte": "^5.37.1",
108
+ "svelte-check": "^4.3.0",
109
+ "typescript": "^5.8.3",
110
+ "typescript-eslint": "^8.38.0",
111
+ "vite": "^7.0.6",
112
+ "vitest": "^3.2.4"
99
113
  },
100
114
  "peerDependencies": {
101
115
  "svelte": "^5.0.0"
102
116
  },
103
117
  "volta": {
104
- "node": "22.13.0"
118
+ "node": "22.16.0"
105
119
  },
106
120
  "publishConfig": {
107
121
  "access": "public"
108
122
  },
109
- "overrides": {
110
- "@sveltejs/kit": {
111
- "cookie": "^0.7.0"
112
- }
113
- },
114
123
  "tags": [
115
124
  "svelte",
116
125
  "virtual-list",