capNodes function
Trim a node set to maxNodes (0 = no cap), preserving nodes' order.
A node in live (the current capture) is never evicted: it is on screen
and the agent is about to be asked about it. Evicting it meant the next
capture re-added it as new, re-dirtied the file and re-uploaded it, on
every capture of a large feed (Android's ScreenMerge has the same rule).
Victims come from the rest, fewest sightings first, then least recently
seen — recurring STRUCTURE outlives one-off DATA rows.
If the capture alone exceeds maxNodes, the first maxNodes live nodes
in nodes' order are kept — already-cached ones first, so an identical
capture keeps exactly the same set.
Implementation
List<CachedNode> capNodes(
List<CachedNode> nodes,
int maxNodes, {
Set<String> live = const {},
}) {
if (maxNodes <= 0 || nodes.length <= maxNodes) return nodes;
final keep = <Object?>{};
for (final n in nodes) {
if (keep.length >= maxNodes) break;
if (live.contains(n['id'])) keep.add(n['id']);
}
final room = maxNodes - keep.length;
if (room > 0) {
final rest = [
for (final n in nodes)
if (!live.contains(n['id'])) n
]..sort((a, b) {
final ac = a['_seenCount'];
final bc = b['_seenCount'];
final byCount = (bc is int ? bc : 0).compareTo(ac is int ? ac : 0);
if (byCount != 0) return byCount;
final byRecency =
'${b['_lastSeen'] ?? ''}'.compareTo('${a['_lastSeen'] ?? ''}');
if (byRecency != 0) return byRecency;
return '${a['id']}'.compareTo('${b['id']}');
});
for (final n in rest.take(room)) {
keep.add(n['id']);
}
}
return [
for (final n in nodes)
if (keep.contains(n['id'])) n
];
}