@humanspeak/svelte-virtual-list 0.4.2 → 0.4.4

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.
@@ -178,8 +178,11 @@
178
178
  import { onMount, tick, untrack } from 'svelte'
179
179
 
180
180
  const rafSchedule = createRafScheduler()
181
- // Per-instance correction guard to avoid same-frame tug-of-war per viewport
182
- const GLOBAL_CORRECTION_COOLDOWN = 16
181
+ // Timing constants
182
+ const GLOBAL_CORRECTION_COOLDOWN_MS = 16
183
+ const SCROLL_IDLE_DELAY_MS = 250
184
+ const SUPPRESSION_WINDOW_MS = 450
185
+ const HEIGHT_DEBOUNCE_MS = 100
183
186
  const lastCorrectionTimestampByViewport = new WeakMap<HTMLElement, number>()
184
187
  // Package-specific debug flag - safe for library distribution
185
188
  // Enable with: PUBLIC_SVELTE_VIRTUAL_LIST_DEBUG=true (preferred) or SVELTE_VIRTUAL_LIST_DEBUG=true
@@ -494,7 +497,7 @@
494
497
  const now = performance.now()
495
498
  const viewportEl = heightManager.viewport
496
499
  const lastCorrectionMs = lastCorrectionTimestampByViewport.get(viewportEl) ?? 0
497
- if (now - lastCorrectionMs < GLOBAL_CORRECTION_COOLDOWN) {
500
+ if (now - lastCorrectionMs < GLOBAL_CORRECTION_COOLDOWN_MS) {
498
501
  suppressBottomAnchoringUntilMs = now + 50
499
502
  return
500
503
  }
@@ -524,11 +527,12 @@
524
527
  // Note: `container: 'nearest'` option could replace this once browser support improves
525
528
  const currentScrollTop = heightManager.viewport.scrollTop
526
529
  const offset = itemRect.bottom - contRect.bottom
527
- heightManager.viewport.scrollTop = currentScrollTop + offset
530
+ syncScrollTop(currentScrollTop + offset)
528
531
  log('[SVL] b2t-correction-manual', { offset })
532
+ } else {
533
+ // Sync our internal scroll state with actual DOM position
534
+ heightManager.scrollTop = heightManager.viewport.scrollTop
529
535
  }
530
- // Sync our internal scroll state with actual DOM position
531
- heightManager.scrollTop = heightManager.viewport.scrollTop
532
536
  // After peer correction, delay further corrections briefly
533
537
  suppressBottomAnchoringUntilMs = performance.now() + 200
534
538
  }
@@ -688,7 +692,7 @@
688
692
  })
689
693
  heightManager.endDynamicUpdate()
690
694
  },
691
- lastMeasuredIndex < 0 || dirtyItems.size > 0 ? 0 : 100, // debounceTime (no debounce on first pass or when dirty items exist)
695
+ lastMeasuredIndex < 0 || dirtyItems.size > 0 ? 0 : HEIGHT_DEBOUNCE_MS,
692
696
  dirtyItems, // Pass dirty items for processing
693
697
  0, // Don't pass ReactiveListManager state - let each system manage its own totals
694
698
  0, // Don't pass ReactiveListManager state - let each system manage its own totals
@@ -1124,7 +1128,7 @@
1124
1128
  if (idleCorrectionsOnly || anchorModeEnabled) {
1125
1129
  reconcileToAnchorIfEnabled()
1126
1130
  }
1127
- }, 250)
1131
+ }, SCROLL_IDLE_DELAY_MS)
1128
1132
 
1129
1133
  rafSchedule(() => {
1130
1134
  const current = heightManager.viewport.scrollTop
@@ -1132,7 +1136,7 @@
1132
1136
  const delta = lastScrollTopSnapshot - current
1133
1137
  if (delta > 0.5) {
1134
1138
  // Widen suppression to avoid fighting peer instance corrections
1135
- suppressBottomAnchoringUntilMs = performance.now() + 450
1139
+ suppressBottomAnchoringUntilMs = performance.now() + SUPPRESSION_WINDOW_MS
1136
1140
  userHasScrolledAway = true
1137
1141
  }
1138
1142
  }
@@ -183,11 +183,6 @@ export declare class ReactiveListManager {
183
183
  * @returns Array of cumulative block sums
184
184
  */
185
185
  getBlockSums(): number[];
186
- /**
187
- * Build block prefix sums for efficient offset calculations.
188
- * Uses the same algorithm as the utility function but leverages internal state.
189
- */
190
- private buildBlockSums;
191
186
  /**
192
187
  * Create a new ReactiveListManager instance
193
188
  *
@@ -1,3 +1,4 @@
1
+ import { buildBlockSums } from '../utils/virtualList.js';
1
2
  import { RecomputeScheduler } from './RecomputeScheduler.js';
2
3
  /**
3
4
  * ReactiveListManager - A standalone reactive height calculation system
@@ -378,30 +379,11 @@ export class ReactiveListManager {
378
379
  */
379
380
  getBlockSums() {
380
381
  if (!this._blockSumsValid || this._blockSums.length === 0) {
381
- this._blockSums = this.buildBlockSums();
382
+ this._blockSums = buildBlockSums(this._heightCache, this._averageHeight, this._itemLength, this._blockSize);
382
383
  this._blockSumsValid = true;
383
384
  }
384
385
  return this._blockSums;
385
386
  }
386
- /**
387
- * Build block prefix sums for efficient offset calculations.
388
- * Uses the same algorithm as the utility function but leverages internal state.
389
- */
390
- buildBlockSums() {
391
- const blocks = Math.ceil(this._itemLength / this._blockSize);
392
- const sums = new Array(Math.max(0, blocks - 1));
393
- let running = 0;
394
- for (let b = 0; b < blocks - 1; b++) {
395
- const start = b * this._blockSize;
396
- const end = start + this._blockSize;
397
- for (let i = start; i < end; i++) {
398
- const height = this._heightCache[i];
399
- running += Number.isFinite(height) && height > 0 ? height : this._averageHeight;
400
- }
401
- sums[b] = running;
402
- }
403
- return sums;
404
- }
405
387
  /**
406
388
  * Create a new ReactiveListManager instance
407
389
  *
@@ -138,33 +138,6 @@ export declare const calculateAverageHeight: (itemElements: HTMLElement[], visib
138
138
  delta: number;
139
139
  }>;
140
140
  };
141
- /**
142
- * Processes large arrays in chunks to prevent UI blocking.
143
- *
144
- * This function implements a progressive processing strategy that:
145
- * 1. Breaks down large arrays into manageable chunks
146
- * 2. Processes each chunk asynchronously
147
- * 3. Reports progress after each chunk
148
- * 4. Yields to the main thread between chunks
149
- *
150
- * @param {any[]} items - Array of items to process
151
- * @param {number} chunkSize - Number of items to process in each chunk
152
- * @param {(processed: number) => void} onProgress - Callback for progress updates
153
- * @param {() => void} onComplete - Callback when all processing is complete
154
- *
155
- * @returns {Promise<void>} Resolves when all chunks have been processed
156
- *
157
- * @example
158
- * await processChunked(
159
- * largeArray,
160
- * 50,
161
- * (processed) => console.log(`Processed ${processed} items`),
162
- * () => console.log('All items processed')
163
- * )
164
- */
165
- export declare const processChunked: (items: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
166
- chunkSize: number, onProgress: (processed: number) => void, // eslint-disable-line no-unused-vars
167
- onComplete: () => void) => Promise<void>;
168
141
  /**
169
142
  * Calculates the scroll offset (in pixels) needed to bring a specific item into view in a virtual list.
170
143
  *
@@ -16,6 +16,7 @@
16
16
  * ```
17
17
  */
18
18
  export const getValidHeight = (height, fallback) => Number.isFinite(height) && height > 0 ? height : fallback;
19
+ const BOTTOM_TOLERANCE_FACTOR = 0.25;
19
20
  /**
20
21
  * Clamps a numeric value to be within a specified range.
21
22
  *
@@ -97,7 +98,7 @@ export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, tot
97
98
  const totalHeight = totalContentHeight ?? totalItems * itemHeight;
98
99
  const maxScrollTop = Math.max(0, totalHeight - viewportHeight);
99
100
  // Use strict tolerance to avoid premature bottom anchoring that leaves a visible gap
100
- const tolerance = Math.max(1, Math.floor(itemHeight * 0.25)); // pixels, adaptive for wrong initial sizes
101
+ const tolerance = Math.max(1, Math.floor(itemHeight * BOTTOM_TOLERANCE_FACTOR));
101
102
  const isAtBottom = Math.abs(scrollTop - maxScrollTop) <= tolerance;
102
103
  if (isAtBottom) {
103
104
  // Pack from the end using measured heights when available: walk backward until viewport filled
@@ -335,49 +336,6 @@ export const calculateAverageHeight = (itemElements, visibleRange, heightCache,
335
336
  heightChanges
336
337
  };
337
338
  };
338
- /**
339
- * Processes large arrays in chunks to prevent UI blocking.
340
- *
341
- * This function implements a progressive processing strategy that:
342
- * 1. Breaks down large arrays into manageable chunks
343
- * 2. Processes each chunk asynchronously
344
- * 3. Reports progress after each chunk
345
- * 4. Yields to the main thread between chunks
346
- *
347
- * @param {any[]} items - Array of items to process
348
- * @param {number} chunkSize - Number of items to process in each chunk
349
- * @param {(processed: number) => void} onProgress - Callback for progress updates
350
- * @param {() => void} onComplete - Callback when all processing is complete
351
- *
352
- * @returns {Promise<void>} Resolves when all chunks have been processed
353
- *
354
- * @example
355
- * await processChunked(
356
- * largeArray,
357
- * 50,
358
- * (processed) => console.log(`Processed ${processed} items`),
359
- * () => console.log('All items processed')
360
- * )
361
- */
362
- export const processChunked = async (items, // eslint-disable-line @typescript-eslint/no-explicit-any
363
- chunkSize, onProgress, // eslint-disable-line no-unused-vars
364
- onComplete) => {
365
- if (!items.length) {
366
- onComplete();
367
- return;
368
- }
369
- const processChunk = async (startIdx) => {
370
- const endIdx = Math.min(startIdx + chunkSize, items.length);
371
- onProgress(endIdx);
372
- if (endIdx < items.length) {
373
- setTimeout(() => processChunk(endIdx), 0);
374
- }
375
- else {
376
- onComplete();
377
- }
378
- };
379
- await processChunk(0);
380
- };
381
339
  /**
382
340
  * Calculates the scroll offset (in pixels) needed to bring a specific item into view in a virtual list.
383
341
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-virtual-list",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
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",
@@ -59,45 +59,45 @@
59
59
  "esm-env": "^1.2.2"
60
60
  },
61
61
  "devDependencies": {
62
- "@eslint/compat": "^2.0.2",
62
+ "@eslint/compat": "^2.0.3",
63
63
  "@eslint/js": "^10.0.1",
64
- "@faker-js/faker": "^10.3.0",
64
+ "@faker-js/faker": "^10.4.0",
65
65
  "@playwright/cli": "^0.1.1",
66
66
  "@playwright/test": "^1.58.2",
67
67
  "@sveltejs/adapter-auto": "^7.0.1",
68
- "@sveltejs/kit": "^2.53.4",
68
+ "@sveltejs/kit": "^2.55.0",
69
69
  "@sveltejs/package": "^2.5.7",
70
- "@sveltejs/vite-plugin-svelte": "^6.2.4",
71
- "@tailwindcss/vite": "^4.2.1",
70
+ "@sveltejs/vite-plugin-svelte": "^7.0.0",
71
+ "@tailwindcss/vite": "^4.2.2",
72
72
  "@testing-library/jest-dom": "^6.9.1",
73
73
  "@testing-library/svelte": "^5.3.1",
74
74
  "@testing-library/user-event": "^14.6.1",
75
- "@types/node": "^25.3.2",
76
- "@typescript-eslint/eslint-plugin": "^8.56.1",
77
- "@typescript-eslint/parser": "^8.56.1",
78
- "@vitest/coverage-v8": "^4.0.18",
79
- "eslint": "^10.0.2",
75
+ "@types/node": "^25.5.0",
76
+ "@typescript-eslint/eslint-plugin": "^8.57.2",
77
+ "@typescript-eslint/parser": "^8.57.2",
78
+ "@vitest/coverage-v8": "^4.1.2",
79
+ "eslint": "^10.1.0",
80
80
  "eslint-config-prettier": "^10.1.8",
81
81
  "eslint-plugin-import": "^2.32.0",
82
- "eslint-plugin-svelte": "^3.15.0",
82
+ "eslint-plugin-svelte": "^3.16.0",
83
83
  "eslint-plugin-unused-imports": "^4.4.1",
84
- "globals": "^17.3.0",
84
+ "globals": "^17.4.0",
85
85
  "husky": "^9.1.7",
86
- "jsdom": "^28.1.0",
87
- "mprocs": "^0.8.3",
86
+ "jsdom": "^29.0.1",
87
+ "mprocs": "^0.9.2",
88
88
  "prettier": "^3.8.1",
89
89
  "prettier-plugin-organize-imports": "^4.3.0",
90
- "prettier-plugin-svelte": "^3.5.0",
90
+ "prettier-plugin-svelte": "^3.5.1",
91
91
  "prettier-plugin-tailwindcss": "^0.7.2",
92
- "publint": "^0.3.17",
93
- "svelte": "^5.53.6",
94
- "svelte-check": "^4.4.4",
95
- "tailwindcss": "^4.2.1",
92
+ "publint": "^0.3.18",
93
+ "svelte": "^5.55.0",
94
+ "svelte-check": "^4.4.5",
95
+ "tailwindcss": "^4.2.2",
96
96
  "tw-animate-css": "^1.4.0",
97
97
  "typescript": "^5.9.3",
98
- "typescript-eslint": "^8.56.1",
99
- "vite": "^7.3.1",
100
- "vitest": "^4.0.18"
98
+ "typescript-eslint": "^8.57.2",
99
+ "vite": "^8.0.3",
100
+ "vitest": "^4.1.2"
101
101
  },
102
102
  "peerDependencies": {
103
103
  "svelte": "^5.0.0"
@@ -120,6 +120,7 @@
120
120
  ],
121
121
  "scripts": {
122
122
  "build": "vite build && pnpm run package",
123
+ "cf-typegen": "pnpm --filter docs cf-typegen",
123
124
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
124
125
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
125
126
  "dev": "vite dev",