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`);