diff --git a/public/packets.js b/public/packets.js index 12432e2c..5dcf3273 100644 --- a/public/packets.js +++ b/public/packets.js @@ -53,6 +53,7 @@ let _displayPackets = []; // filtered packets for current view let _displayGrouped = false; // whether _displayPackets is in grouped mode let _rowCounts = []; // per-entry DOM row counts (1 for flat, 1+children for expanded groups) + let _rowCountsDirty = false; // set when _rowCounts may be stale (e.g. WS added children) (#410) let _cumulativeOffsetsCache = null; // cached cumulative offsets, invalidated on _rowCounts change let _lastVisibleStart = -1; // last rendered start index (for dirty checking) let _lastVisibleEnd = -1; // last rendered end index (for dirty checking) @@ -396,6 +397,9 @@ existing._children.unshift(p); if (existing._children.length > 200) existing._children.length = 200; sortGroupChildren(existing); + // Invalidate row counts — child count changed, so virtual scroll + // heights are stale until next renderTableRows() (#410) + _invalidateRowCounts(); } } else { // New group @@ -442,6 +446,7 @@ clearTimeout(_wsRenderTimer); _displayPackets = []; _rowCounts = []; + _rowCountsDirty = false; _cumulativeOffsetsCache = null; _observerFilterSet = null; _lastVisibleStart = -1; @@ -1123,6 +1128,21 @@ `; } + // Mark _rowCounts as stale so renderVisibleRows() recomputes them lazily. + // Called when expanded group children change outside renderTableRows() (#410). + function _invalidateRowCounts() { + _rowCountsDirty = true; + _cumulativeOffsetsCache = null; + } + + // Recompute _rowCounts from _displayPackets if they've been invalidated. + function _refreshRowCountsIfDirty() { + if (!_rowCountsDirty || !_displayPackets.length) return; + _rowCounts = _displayPackets.map(function(p) { return _getRowCount(p); }); + _cumulativeOffsetsCache = null; + _rowCountsDirty = false; + } + // Compute the number of DOM rows a single entry produces. // Used by both row counting and renderVisibleRows to avoid divergence (#424). function _getRowCount(p) { @@ -1161,6 +1181,9 @@ const scrollContainer = document.getElementById('pktLeft'); if (!scrollContainer) return; + // Recompute row counts if they were invalidated (e.g. WS added children) (#410) + _refreshRowCountsIfDirty(); + // Compute total DOM rows accounting for expanded groups const offsets = _cumulativeRowOffsets(); const totalDomRows = offsets[offsets.length - 1]; @@ -1317,6 +1340,7 @@ if (!displayPackets.length) { _displayPackets = []; _rowCounts = []; + _rowCountsDirty = false; _cumulativeOffsetsCache = null; _observerFilterSet = null; _lastVisibleStart = -1; @@ -1336,6 +1360,7 @@ _displayGrouped = groupByHash; _observerFilterSet = filters.observer ? new Set(filters.observer.split(',')) : null; _rowCounts = displayPackets.map(p => _getRowCount(p)); + _rowCountsDirty = false; _cumulativeOffsetsCache = null; attachVScrollListener(); @@ -2044,6 +2069,8 @@ renderPath, _getRowCount, _cumulativeRowOffsets, + _invalidateRowCounts, + _refreshRowCountsIfDirty, buildGroupRowHtml, buildFlatRowHtml, }; diff --git a/test-packets.js b/test-packets.js index 94317b40..59586f17 100644 --- a/test-packets.js +++ b/test-packets.js @@ -757,6 +757,33 @@ console.log('\n=== packets.js: page registration ==='); }); } +console.log('\n=== packets.js: _invalidateRowCounts / _refreshRowCountsIfDirty (#410) ==='); +{ + const ctx = loadPacketsSandbox(); + const api = ctx._packetsTestAPI; + + test('_invalidateRowCounts and _refreshRowCountsIfDirty are exported', () => { + assert(typeof api._invalidateRowCounts === 'function'); + assert(typeof api._refreshRowCountsIfDirty === 'function'); + }); + + test('_invalidateRowCounts does not throw', () => { + api._invalidateRowCounts(); + }); + + test('_refreshRowCountsIfDirty does not throw when no display packets', () => { + api._invalidateRowCounts(); + api._refreshRowCountsIfDirty(); + }); + + test('_cumulativeRowOffsets returns valid offsets after invalidation cycle', () => { + // Even with no display packets, should return valid array + const offsets = api._cumulativeRowOffsets(); + assert(Array.isArray(offsets)); + assert(offsets[0] === 0); + }); +} + // ===== SUMMARY ===== console.log(`\n${'='.repeat(40)}`); console.log(`packets.js tests: ${passed} passed, ${failed} failed`);