encodeTree method

Uint8List? encodeTree(
  1. SemanticTree tree, {
  2. SemanticTreeUpdate? update,
})

Encodes tree for the wire, or returns null when the exposed semantics are unchanged since the last send (so a dirty frame that didn't actually alter the accessible tree costs zero bytes).

update names the nodes whose wire form may have changed AND carries the live nodes themselves (nextNodesById); on a patch frame the encoder redacts and re-serializes ONLY those, straight from the live tree — never building a full SemanticInspectionSnapshot. The owner's node equality compares exactly the fields the wire carries — including bounds and child order — so a node pushed to a new position (bounds shift, no model rebuild) still lands in the update. A debug-only oracle re-runs the full flatten every frame and asserts the incrementally-maintained _sent still equals the ground truth, so any gap between the update and the wire form fails loudly in tests.

Implementation

Uint8List? encodeTree(SemanticTree tree, {SemanticTreeUpdate? update}) {
  _rejection = null;
  final rootId = sanitizeForDisplay(tree.root.id.value);
  if (!_sentFull) {
    final flat = _flattenTree(tree);
    return _encodeFull(flat, rootId: rootId, flattenedNodes: flat.length);
  }

  if (update == null) {
    // No update: fall back to the full flatten + structural compare.
    final flat = _flattenTree(tree);
    final encoded = _encodeFlattenedPatch(
      flat,
      rootId: rootId,
      flattenedNodes: flat.length,
    );
    if (_sentFull) _assertTreeOracle(tree);
    return encoded;
  }

  // O(changed): redact + serialize only the nodes the update names. Stage the
  // edits without touching [_sent]; validation and byte accounting happen
  // before commit so a rejected state can only force the NEXT send to FULL,
  // never leave a PATCH base the peer did not receive.
  var flattened = 0;
  final staged = <String, Map<String, Object?>>{};
  final touched = <String>{};
  for (final id in update.added.followedBy(update.updated)) {
    final node = update.nextNodesById[id];
    if (node == null) continue; // named-changed but gone from the tree.
    flattened++;
    final json = SemanticInspectionNode.flattenLiveNode(node);
    final wireId = json['id']! as String;
    touched.add(wireId);
    final prior = staged[wireId] ?? _sent[wireId];
    if (prior == null || !_jsonEquals(prior, json)) {
      staged[wireId] = json;
    }
  }
  final removed = <String>{};
  for (final id in update.removed) {
    final wireId = sanitizeForDisplay(id.value);
    // A changed node may carry this same wire id — a duplicate, or two raw
    // ids that sanitize alike — in which case it is LIVE. Do not delete it.
    if (touched.contains(wireId)) continue;
    if (_sent.containsKey(wireId)) removed.add(wireId);
  }
  final encoded = _finishPatch(
    rootId: rootId,
    staged: staged,
    removed: removed,
    flattenedNodes: flattened,
  );
  if (_sentFull) _assertTreeOracle(tree);
  return encoded;
}