adding packages

This commit is contained in:
2026-01-15 14:38:46 -08:00
parent ef86e3ab6a
commit 6869cf47e5
5253 changed files with 726695 additions and 34 deletions
@@ -0,0 +1,298 @@
import 'dart:math';
import 'dart:typed_data';
enum ChangeOp { insert, delete, modify, equal }
class DiffNode {
final ChangeOp op;
final String key;
final Object? oldValue;
final Object? newValue;
final List<DiffNode>? children;
final TextSplice? splice;
DiffNode.insert(this.key, this.newValue)
: op = ChangeOp.insert,
oldValue = null,
children = null,
splice = null;
DiffNode.delete(this.key, this.oldValue)
: op = ChangeOp.delete,
newValue = null,
children = null,
splice = null;
DiffNode.equal(this.key, this.newValue)
: op = ChangeOp.equal,
oldValue = newValue,
children = null,
splice = null;
DiffNode.modify(
this.key,
this.oldValue,
this.newValue, {
this.children,
this.splice,
}) : op = ChangeOp.modify;
@override
String toString() => '$op: $key';
}
class TextSplice {
final int index;
final int deleteCount;
final String insertText;
TextSplice(this.index, this.deleteCount, this.insertText);
Map<String, dynamic> toJson() => {
'i': index,
'd': deleteCount,
't': insertText,
};
}
class HybridDiffer {
static List<DiffNode> diff(
List<Map<String, Object?>> oldList,
List<Map<String, Object?>> newList, {
required String idField,
}) {
final oldIds = oldList.map((e) => e[idField]).toList();
final newIds = newList.map((e) => e[idField]).toList();
final structuralOps = _myersDiff(oldIds, newIds);
final results = <DiffNode>[];
for (final op in structuralOps) {
if (op.type == _MyersOpType.delete) {
final item = oldList[op.oldIndex!];
results.add(DiffNode.delete(item[idField].toString(), item));
} else if (op.type == _MyersOpType.insert) {
final item = newList[op.newIndex!];
results.add(DiffNode.insert(item[idField].toString(), item));
} else {
// Equal ID: Check Content
final oldItem = oldList[op.oldIndex!];
final newItem = newList[op.newIndex!];
final key = newItem[idField].toString();
if (_areDeepEqual(oldItem, newItem)) {
results.add(DiffNode.equal(key, newItem));
} else {
final fieldChanges = _generateObjectDiff(oldItem, newItem);
results.add(
DiffNode.modify(key, oldItem, newItem, children: fieldChanges),
);
}
}
}
return results;
}
static List<DiffNode> _generateObjectDiff(
Map<String, Object?> oldObj,
Map<String, Object?> newObj,
) {
final diffs = <DiffNode>[];
final allKeys = {...oldObj.keys, ...newObj.keys}.toList()..sort();
for (final key in allKeys) {
final oldVal = oldObj[key];
final newVal = newObj[key];
if (!oldObj.containsKey(key)) {
diffs.add(DiffNode.insert(key, newVal));
} else if (!newObj.containsKey(key)) {
diffs.add(DiffNode.delete(key, oldVal));
} else if (oldVal is Map<String, Object?> &&
newVal is Map<String, Object?>) {
final nested = _generateObjectDiff(oldVal, newVal);
if (nested.isNotEmpty) {
diffs.add(DiffNode.modify(key, oldVal, newVal, children: nested));
}
} else if (oldVal is String && newVal is String) {
if (oldVal != newVal) {
final splice = _calculateStringSplice(oldVal, newVal);
diffs.add(DiffNode.modify(key, oldVal, newVal, splice: splice));
}
} else {
if (!_areDeepEqual(oldVal, newVal)) {
diffs.add(DiffNode.modify(key, oldVal, newVal));
}
}
}
return diffs;
}
static bool _areDeepEqual(Object? a, Object? b) {
if (a == b) return true;
if (a is List && b is List) {
if (a.length != b.length) return false;
for (var i = 0; i < a.length; i++) {
if (!_areDeepEqual(a[i], b[i])) return false;
}
return true;
}
if (a is Map && b is Map) {
if (a.length != b.length) return false;
for (final key in a.keys) {
if (!b.containsKey(key) || !_areDeepEqual(a[key], b[key])) return false;
}
return true;
}
return false;
}
static TextSplice? _calculateStringSplice(String oldText, String newText) {
if (oldText == newText) return null;
int start = 0;
final minLen = min(oldText.length, newText.length);
while (start < minLen &&
oldText.codeUnitAt(start) == newText.codeUnitAt(start)) {
start++;
}
if (start > 0 &&
start < oldText.length &&
_isSurrogate(oldText.codeUnitAt(start - 1))) {
start--;
}
int oldEnd = oldText.length;
int newEnd = newText.length;
while (oldEnd > start &&
newEnd > start &&
oldText.codeUnitAt(oldEnd - 1) == newText.codeUnitAt(newEnd - 1)) {
oldEnd--;
newEnd--;
}
return TextSplice(start, oldEnd - start, newText.substring(start, newEnd));
}
static bool _isSurrogate(int code) => (code >= 0xD800 && code <= 0xDFFF);
// --- MYERS ALGORITHM ---
static List<_MyersOp> _myersDiff(List oldIds, List newIds) {
final n = oldIds.length;
final m = newIds.length;
final max = n + m;
final v = Int32List(2 * max + 1)..fillRange(0, 2 * max + 1, -1);
final trace = <Int32List>[];
v[max] = 0;
for (var d = 0; d <= max; d++) {
trace.add(Int32List.fromList(v));
for (var k = -d; k <= d; k += 2) {
final indexK = max + k;
int x;
if (d == 0) {
x = 0;
} else if (k == -d || (k != d && v[indexK - 1] < v[indexK + 1])) {
x = v[indexK + 1];
} else {
x = v[indexK - 1] + 1;
}
int y = x - k;
while (x < n && y < m && oldIds[x] == newIds[y]) {
x++;
y++;
}
v[indexK] = x;
if (x >= n && y >= m) {
return _buildMyersScript(oldIds, newIds, trace, d, max);
}
}
}
return [];
}
static List<_MyersOp> _buildMyersScript(
List oldIds,
List newIds,
List<Int32List> trace,
int d,
int maxOffset,
) {
var script = <_MyersOp>[];
var x = oldIds.length;
var y = newIds.length;
for (var i = d; i > 0; i--) {
final k = x - y;
final kIndex = maxOffset + k;
final prevV = trace[i];
final kMinus1 = kIndex - 1;
final kPlus1 = kIndex - 1 + 2; // kIndex + 1
int prevKIndex;
final bool pickInsert;
if (k == -i) {
pickInsert = true;
} else if (k == i) {
pickInsert = false;
} else if (prevV[kMinus1] == -1) {
pickInsert = true;
} else if (prevV[kPlus1] == -1) {
pickInsert = false;
} else {
pickInsert = prevV[kMinus1] < prevV[kPlus1];
}
if (pickInsert) {
prevKIndex = kPlus1;
} else {
prevKIndex = kMinus1;
}
final prevX = prevV[prevKIndex];
final isInsert = prevKIndex == kPlus1;
final startX = isInsert ? prevX : prevX + 1;
while (x > startX) {
// Snake loop logic
script.add(
_MyersOp(_MyersOpType.equal, oldIndex: x - 1, newIndex: y - 1),
);
x--;
y--;
}
if (isInsert) {
// Moved down (Insert)
script.add(_MyersOp(_MyersOpType.insert, newIndex: y - 1));
y--;
} else {
// Moved right (Delete)
script.add(_MyersOp(_MyersOpType.delete, oldIndex: x - 1));
x--;
}
}
// Flush remaining snakes
while (x > 0 && y > 0) {
script.add(
_MyersOp(_MyersOpType.equal, oldIndex: x - 1, newIndex: y - 1),
);
x--;
y--;
}
return script.reversed.toList();
}
}
enum _MyersOpType { insert, delete, equal }
class _MyersOp {
final _MyersOpType type;
final int? oldIndex;
final int? newIndex;
_MyersOp(this.type, {this.oldIndex, this.newIndex});
}
@@ -0,0 +1,279 @@
import 'dart:typed_data';
import 'package:collection/collection.dart';
enum DiffType { insert, delete, equal }
class DiffOperation<T> {
final DiffType type;
final T data;
final int
index; // Index in newList (for insert) or oldList (for delete/equal)
DiffOperation(this.type, this.data, this.index);
@override
String toString() =>
'$type: ${data.toString().substring(0, 20)}... (@$index)';
}
/// A generic-free typedef for the custom hasher to maximize performance
typedef JsonHasher = int Function(Map<String, dynamic> item);
class FastJsonDiffer {
static const _deepEquality = DeepCollectionEquality();
// POOLING: Reusable buffers to prevent GC thrashing during high-freq updates
Int32List _vBuffer = Int32List(1024);
// Storing history for the traceback
// We reuse the list container, but we must allocate new Int32Lists for snapshots
final List<Int32List> _traceBuffer = [];
/// Main Diff Method.
///
/// [keyGenerator] (Optional): Provide this for maximum speed.
/// instead of comparing every field, return a hash of specific fields
/// (e.g., `(i) => Object.hash(i['id'], i['version'])`).
List<DiffOperation<Map<String, dynamic>>> diff(
List<Map<String, dynamic>> oldList,
List<Map<String, dynamic>> newList, {
JsonHasher? keyGenerator,
}) {
// 1. Identity Check (Optimization)
if (identical(oldList, newList)) return [];
if (oldList.isEmpty && newList.isEmpty) return [];
// 2. Generate Proxies (Hashes)
// If no keyGenerator is provided, we default to expensive Deep Equality
final hasher = keyGenerator ?? (item) => _deepEquality.hash(item);
final oldHashes = _generateHashes(oldList, hasher);
final newHashes = _generateHashes(newList, hasher);
// 3. Run Myers Algorithm on Ints
final rawOps = _diffInts(oldHashes, newHashes);
// 4. Rehydrate (Map indices back to actual Objects)
return rawOps.map((op) {
switch (op.type) {
case DiffType.insert:
return DiffOperation(DiffType.insert, newList[op.index], op.index);
case DiffType.delete:
return DiffOperation(DiffType.delete, oldList[op.index], op.index);
case DiffType.equal:
return DiffOperation(DiffType.equal, oldList[op.index], op.index);
}
}).toList();
}
Int32List _generateHashes(
List<Map<String, dynamic>> list,
JsonHasher hasher,
) {
final int len = list.length;
final buffer = Int32List(len);
for (var i = 0; i < len; i++) {
buffer[i] = hasher(list[i]);
}
return buffer;
}
/// The Core Myers Logic specialized for Int32List.
/// This runs purely on stack primitives and typed arrays.
List<DiffOperation<int>> _diffInts(Int32List oldList, Int32List newList) {
final n = oldList.length;
final m = newList.length;
final max = n + m;
final requiredSize = 2 * max + 1;
// Grow buffer if necessary
if (_vBuffer.length < requiredSize) {
_vBuffer = Int32List(requiredSize);
}
_vBuffer.fillRange(0, requiredSize, -1);
_traceBuffer.clear();
_vBuffer[max] = 0;
for (var d = 0; d <= max; d++) {
// Snapshot current V state for traceback.
_traceBuffer.add(Int32List.fromList(_vBuffer.sublist(0, requiredSize)));
for (var k = -d; k <= d; k += 2) {
final kIndex = max + k;
int x;
// Choose move: Down (Insertion) or Right (Deletion)
if (d == 0) {
x = 0;
} else if (k == -d ||
(k != d && _vBuffer[kIndex - 1] < _vBuffer[kIndex + 1])) {
x = _vBuffer[kIndex + 1];
} else {
x = _vBuffer[kIndex - 1] + 1;
}
int y = x - k;
// Snake: Move diagonal as long as hashes match
while (x < n && y < m && oldList[x] == newList[y]) {
x++;
y++;
}
_vBuffer[kIndex] = x;
// Check for completion
if (x >= n && y >= m) {
return _buildScript(oldList, newList, d, max);
}
}
}
return [];
}
List<DiffOperation<int>> _buildScript(
Int32List oldList,
Int32List newList,
int d,
int maxOffset,
) {
var script = <DiffOperation<int>>[];
var x = oldList.length;
var y = newList.length;
for (var i = d; i > 0; i--) {
final k = x - y;
final kIndex = maxOffset + k;
final prevV = _traceBuffer[i]; // FIXED: Use trace[i]
final kMinus1 = kIndex - 1;
final kPlus1 = kIndex + 1;
int prevKIndex;
final bool pickInsert;
// Robust selection logic
if (k == -i) {
pickInsert = true;
} else if (k == i) {
pickInsert = false;
} else if (prevV[kMinus1] == -1) {
pickInsert = true;
} else if (prevV[kPlus1] == -1) {
pickInsert = false;
} else {
pickInsert = prevV[kMinus1] < prevV[kPlus1];
}
if (pickInsert) {
prevKIndex = kPlus1;
} else {
prevKIndex = kMinus1;
}
final prevX = prevV[prevKIndex];
// Assert specific invariants
assert(
prevX >= 0,
"Myers invariant violated: prevX should effectively never be -1 (sentinel) when accessed from valid path.",
);
assert(
x >= prevX,
"Myers invariant violated: x ($x) cannot be less than prevX ($prevX)",
);
final isInsert = prevKIndex == kPlus1;
final startX = isInsert ? prevX : prevX + 1;
// Add Snakes (Equal)
while (x > startX) {
assert(x > 0, "Backtracking snake cannot go below 0 for x");
script.add(DiffOperation(DiffType.equal, 0, x - 1));
x--;
y--;
}
// Add Edit
if (isInsert) {
assert(y > 0, "Insert operation must have y index > 0 (y=$y)");
script.add(DiffOperation(DiffType.insert, 0, y - 1));
y--;
} else {
assert(x > 0, "Delete operation must have x index > 0 (x=$x)");
script.add(DiffOperation(DiffType.delete, 0, x - 1));
x--;
}
}
// Flush remaining snakes
while (x > 0 && y > 0) {
script.add(DiffOperation(DiffType.equal, 0, x - 1));
x--;
y--;
}
assert(
x == 0 && y == 0,
"Backtrack should end at 0,0. Ended at x=$x, y=$y. Traceback logic is flowed.",
);
return script.reversed.toList();
}
}
void main() {
final differ = FastJsonDiffer();
// Scenario: API Response or Game State
final oldData = [
{'id': 1, 'val': 'A', 'meta': 'complex_obj'},
{'id': 2, 'val': 'B', 'meta': 'complex_obj'},
{'id': 3, 'val': 'C', 'meta': 'complex_obj'},
];
final newData = [
{'id': 1, 'val': 'A', 'meta': 'complex_obj'},
{'id': 3, 'val': 'Z', 'meta': 'complex_obj'}, // Modified (val C -> Z)
{'id': 4, 'val': 'D', 'meta': 'complex_obj'}, // Inserted
];
print('--- 1. Default (Deep Equality) ---');
// Good for correctness, slower for large lists.
final opsDefault = differ.diff(oldData, newData);
for (var op in opsDefault) {
if (op.type != DiffType.equal) print(op);
}
// Expected:
// Delete: {id: 2...}
// Delete: {id: 3, val: C...} (Because val changed, hash changed)
// Insert: {id: 3, val: Z...}
// Insert: {id: 4...}
print('\n--- 2. Optimized (ID + Val Check) ---');
// Great for high-frequency. We tell the differ EXACTLY what constitutes a change.
// Here we ignore the 'meta' field entirely.
final opsFast = differ.diff(
oldData,
newData,
keyGenerator: (map) => Object.hash(map['id'], map['val']),
);
for (var op in opsFast) {
if (op.type != DiffType.equal) print(op);
}
print('\n--- 3. Ultra Fast (ID Only - Move Detection) ---');
// If we only care if the ID exists (e.g. for list animations where content updates later)
final opsIdOnly = differ.diff(
oldData,
newData,
keyGenerator: (map) => map['id'] as int, // Direct int cast is fastest
);
for (var op in opsIdOnly) {
if (op.type != DiffType.equal) print(op);
}
}
@@ -0,0 +1,163 @@
import 'dart:convert';
// =============================================================================
// --- CORE ALGORITHM (The Diff Engine) ---
// =============================================================================
/// Sentinel value for deleted keys
class Deleted {
const Deleted();
Map<String, dynamic> toJson() => {'_op': 'd'};
@override
String toString() => '<Deleted>';
}
const deleted = Deleted();
/// Represents a precise text edit (Splice)
class TextSplice {
final int index;
final int deleteCount;
final String insertText;
TextSplice(this.index, this.deleteCount, this.insertText);
/// Applies the splice to a string
String apply(String original) {
if (index > original.length) return original + insertText;
final start = original.substring(0, index);
final end = original.substring(index + deleteCount);
return '$start$insertText$end';
}
Map<String, dynamic> toJson() => {
'_op': 's',
'i': index,
'd': deleteCount,
't': insertText,
};
}
class DiffEngine {
/// Generates a recursive diff between two JSON-like maps
static Map<String, dynamic> generateDiff(
Map<String, dynamic> oldObj,
Map<String, dynamic> newObj,
) {
final diff = <String, dynamic>{};
// 1. Walk new keys
for (final key in newObj.keys) {
final oldVal = oldObj[key];
final newVal = newObj[key];
// New Key Added
if (!oldObj.containsKey(key)) {
diff[key] = newVal;
continue;
}
// Recursive Map Diff
if (oldVal is Map<String, dynamic> && newVal is Map<String, dynamic>) {
final nested = generateDiff(oldVal, newVal);
if (nested.isNotEmpty) diff[key] = nested;
continue;
}
// Smart String Diff (Splice)
if (oldVal is String && newVal is String) {
final splice = _calculateStringSplice(oldVal, newVal);
if (splice != null) diff[key] = splice;
continue;
}
// Standard Value Replacement (Primitives or Lists)
if (!_areValuesEqual(oldVal, newVal)) {
diff[key] = newVal;
}
}
// 2. Walk old keys to find Deletions
for (final key in oldObj.keys) {
if (!newObj.containsKey(key)) diff[key] = deleted;
}
return diff;
}
/// Calculates the minimal edit (splice) with EMOJI SAFETY
static TextSplice? _calculateStringSplice(String oldText, String newText) {
if (oldText == newText) return null;
// A. Find Common Prefix
int start = 0;
final minLen = (oldText.length < newText.length)
? oldText.length
: newText.length;
while (start < minLen &&
oldText.codeUnitAt(start) == newText.codeUnitAt(start)) {
start++;
}
// SAFETY: If we stopped inside a Surrogate Pair (High Surrogate), backtrack.
if (start > 0 && start < oldText.length && start < newText.length) {
final prevCode = oldText.codeUnitAt(start - 1);
if (prevCode >= 0xD800 && prevCode <= 0xDBFF) {
start--;
}
}
// B. Find Common Suffix
int oldEnd = oldText.length;
int newEnd = newText.length;
while (oldEnd > start &&
newEnd > start &&
oldText.codeUnitAt(oldEnd - 1) == newText.codeUnitAt(newEnd - 1)) {
oldEnd--;
newEnd--;
}
// SAFETY: If we stopped inside a Surrogate Pair (Low Surrogate), expand the change area.
if (oldEnd < oldText.length && newEnd < newText.length) {
final code = oldText.codeUnitAt(oldEnd);
if (code >= 0xDC00 && code <= 0xDFFF) {
oldEnd++;
newEnd++;
}
}
final deleteCount = oldEnd - start;
final insertText = newText.substring(start, newEnd);
return TextSplice(start, deleteCount, insertText);
}
static bool _areValuesEqual(dynamic a, dynamic b) {
if (a == b) return true;
if (a is List && b is List) {
if (a.length != b.length) return false;
for (var i = 0; i < a.length; i++) if (a[i] != b[i]) return false;
return true;
}
return false;
}
}
/// Helper to serialize the diff for the UI/Network
String serializeDiff(Map<String, dynamic> diff) {
// We use a custom encoder to handle our TextSplice and Deleted objects
return const JsonEncoder.withIndent(' ').convert(
jsonDecode(
jsonEncode(
diff,
toEncodable: (obj) {
if (obj is TextSplice) return obj.toJson();
if (obj is Deleted) return obj.toJson();
return obj;
},
),
),
);
}