mergeNodeUnion function
NodeMergeResult
mergeNodeUnion(
- List<
CachedNode> ? existing, - List<
CachedNode> incoming, - String ts, {
- DateTime? staleBefore,
- int maxNodes = 0,
Union the incoming capture into the cached node set, keyed on id, then
cap it at maxNodes (0 = uncapped).
When staleBefore is non-null the capture is eligible to prove absence:
a cached node missing from incoming gets kMissingSinceKey stamped (if
it has none), and one whose stamp is at or before staleBefore is pruned.
The cap (see capNodes) never evicts a node present in incoming unless
the capture alone exceeds maxNodes.
changed is measured on the KEPT set: an id added, an id removed (pruned
or evicted), or a kept node whose wire fields moved. A node the cap drops
is not "new" — otherwise an over-cap capture would re-dirty the file on
every identical capture forever.
Implementation
NodeMergeResult mergeNodeUnion(
List<CachedNode>? existing,
List<WireNode> incoming,
String ts, {
DateTime? staleBefore,
int maxNodes = 0,
}) {
final prevById = <String, CachedNode>{};
if (existing != null) {
for (final n in existing) {
final id = n['id'];
if (id is String) prevById[id] = n;
}
}
final incById = <String, WireNode>{};
for (final inc in incoming) {
final id = inc['id'];
if (id is String && id.isNotEmpty) incById[id] = inc;
}
// Existing order first, then new ids in capture order: a stable order keeps
// the file's content hash stable across merges.
final byId = <String, CachedNode>{};
for (final e in prevById.entries) {
final id = e.key;
final n = e.value;
final inc = incById[id];
if (inc != null) {
final seen = n['_seenCount'];
byId[id] = <String, dynamic>{
...inc,
'_firstSeen': n['_firstSeen'] ?? ts,
'_lastSeen': ts,
'_seenCount': (seen is int ? seen : 0) + 1,
};
continue;
}
if (staleBefore == null) {
byId[id] = Map<String, dynamic>.of(n);
continue;
}
final stamp = n[kMissingSinceKey];
final missingSince = stamp is String ? DateTime.tryParse(stamp) : null;
if (missingSince == null) {
// First observed absence: start the clock, prune nothing yet.
byId[id] = <String, dynamic>{...n, kMissingSinceKey: ts};
} else if (!missingSince.isAfter(staleBefore)) {
// Absent from eligible changing captures spanning >= kStaleNodeMaxAge.
continue;
} else {
byId[id] = Map<String, dynamic>.of(n);
}
}
for (final e in incById.entries) {
if (byId.containsKey(e.key)) continue;
byId[e.key] = <String, dynamic>{
...e.value,
'_firstSeen': ts,
'_lastSeen': ts,
'_seenCount': 1,
};
}
final all = byId.values.toList(growable: false);
final kept = capNodes(all, maxNodes, live: incById.keys.toSet());
var changed = kept.length != prevById.length;
if (!changed) {
for (final n in kept) {
final id = n['id'] as String;
final prev = prevById[id];
if (prev == null) {
changed = true;
break;
}
final inc = incById[id];
if (inc != null && !sameWireFields(prev, inc)) {
changed = true;
break;
}
}
}
return NodeMergeResult(kept, changed);
}