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
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -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;
},
),
),
);
}
@@ -0,0 +1,11 @@
name: diff_algorithims
description: A set of diffing algorithims for Dart
version: 0.0.1
homepage: https://github.com/example/pocketbase_sync
publish_to: 'none'
environment:
sdk: ^3.5.0
flutter: ^3.38.5
dev_dependencies:
test: ^1.26.3
@@ -0,0 +1,225 @@
import 'package:diff_algorithims/hybrid_diff.dart';
import 'package:test/test.dart';
void main() {
group('HybridDiffer Structural Tests (Myers)', () {
test('Identical lists return no changes', () {
final list = [
{'id': 1, 'val': 'a'},
{'id': 2, 'val': 'b'},
];
final changes = HybridDiffer.diff(list, list, idField: 'id');
// We expect equal nodes or no nodes depending on filtering.
// The implementation returns Equal nodes for matches.
expect(changes.every((c) => c.op == ChangeOp.equal), isTrue);
expect(changes.length, equals(2));
});
test('Detects simple insertion', () {
final oldList = [
{'id': 1, 'val': 'a'},
];
final newList = [
{'id': 1, 'val': 'a'},
{'id': 2, 'val': 'b'},
];
final changes = HybridDiffer.diff(oldList, newList, idField: 'id');
expect(changes.length, equals(2));
expect(changes[0].op, equals(ChangeOp.equal));
expect(changes[1].op, equals(ChangeOp.insert));
expect(changes[1].key, equals('2'));
});
test('Detects simple deletion', () {
final oldList = [
{'id': 1, 'val': 'a'},
{'id': 2, 'val': 'b'},
];
final newList = [
{'id': 1, 'val': 'a'},
];
final changes = HybridDiffer.diff(oldList, newList, idField: 'id');
expect(changes.length, equals(2));
expect(changes[0].op, equals(ChangeOp.equal));
expect(changes[1].op, equals(ChangeOp.delete));
expect(changes[1].key, equals('2'));
});
test('Detects moves (Delete + Insert)', () {
// Myers detects moves as a delete of the old position and insert at new
final oldList = [
{'id': 1},
{'id': 2},
{'id': 3},
];
final newList = [
{'id': 1},
{'id': 3},
{'id': 2},
]; // 2 moved to end
final changes = HybridDiffer.diff(oldList, newList, idField: 'id');
// Expected: Equal(1), Equal(3), Insert(2), Delete(2) OR Equal(1), Delete(2), Equal(3), Insert(2)
// Myers usually prefers deletes first if weights are equal, but let's check operations.
final ops = changes.map((c) => c.op).toList();
expect(ops, contains(ChangeOp.insert));
expect(ops, contains(ChangeOp.delete));
final insertNode = changes.firstWhere((c) => c.op == ChangeOp.insert);
final deleteNode = changes.firstWhere((c) => c.op == ChangeOp.delete);
expect(insertNode.key, equals(deleteNode.key));
expect(['2', '3'], contains(insertNode.key));
});
});
group('HybridDiffer Content Tests (Deep Diff)', () {
test('Detects modification in primitive fields', () {
final oldList = [
{'id': 1, 'name': 'Alice'},
];
final newList = [
{'id': 1, 'name': 'Bob'},
];
final changes = HybridDiffer.diff(oldList, newList, idField: 'id');
expect(changes.length, equals(1));
final mod = changes.first;
expect(mod.op, equals(ChangeOp.modify));
expect(mod.children, isNotNull);
expect(mod.children!.length, equals(1));
final fieldChange = mod.children!.first;
expect(fieldChange.key, equals('name'));
expect(fieldChange.oldValue, equals('Alice'));
expect(fieldChange.newValue, equals('Bob'));
});
test('Detects nested map changes recursively', () {
final oldList = [
{
'id': 1,
'meta': {'ver': 1, 'author': 'me'},
},
];
final newList = [
{
'id': 1,
'meta': {'ver': 2, 'author': 'me'},
},
];
final changes = HybridDiffer.diff(oldList, newList, idField: 'id');
final rootMod = changes.first;
// Look inside the 'meta' field
final metaMod = rootMod.children!.firstWhere((c) => c.key == 'meta');
expect(metaMod.op, equals(ChangeOp.modify));
// Look inside 'ver' field
final verMod = metaMod.children!.firstWhere((c) => c.key == 'ver');
expect(verMod.oldValue, equals(1));
expect(verMod.newValue, equals(2));
});
test('Handles null values correctly', () {
final oldList = [
{'id': 1, 'val': null},
];
final newList = [
{'id': 1, 'val': 'not null'},
];
final changes = HybridDiffer.diff(oldList, newList, idField: 'id');
final fieldChange = changes.first.children!.first;
expect(fieldChange.key, equals('val'));
expect(fieldChange.oldValue, isNull);
expect(fieldChange.newValue, equals('not null'));
});
});
group('String Splice Optimization', () {
test('Generates TextSplice for string changes', () {
final oldList = [
{'id': 1, 'text': 'Hello World'},
];
final newList = [
{'id': 1, 'text': 'Hello Flutter World'},
];
final changes = HybridDiffer.diff(oldList, newList, idField: 'id');
final fieldChange = changes.first.children!.first;
expect(fieldChange.key, equals('text'));
expect(fieldChange.splice, isNotNull);
// "Hello " (len 6) match. Insert "Flutter ".
expect(fieldChange.splice!.index, equals(6));
expect(fieldChange.splice!.deleteCount, equals(0));
expect(fieldChange.splice!.insertText, equals('Flutter '));
});
test('Handles Unicode Surrogate Pairs (Emoji Safety)', () {
// 🤚 is \uD83E\uDD1A (2 code units)
final oldList = [
{'id': 1, 'text': 'Hi 🤚'},
];
final newList = [
{'id': 1, 'text': 'Hi 🤚 there'},
];
final changes = HybridDiffer.diff(oldList, newList, idField: 'id');
final fieldChange = changes.first.children!.first;
expect(fieldChange.splice, isNotNull);
// Ensure we didn't split the emoji. The index should differ by proper length.
// 'Hi ' is 3. Emoji is 2. Total 5.
expect(fieldChange.splice!.index, greaterThanOrEqualTo(5));
expect(fieldChange.splice!.insertText, contains('there'));
});
});
group('Performance / Stress Test', () {
test('Handles large lists efficiently', () {
// Generate 5000 items
final oldList = List<Map<String, dynamic>>.generate(
5000,
(i) => {'id': i, 'val': 'item $i'},
);
final newList = List<Map<String, dynamic>>.from(oldList);
// Make 3 changes
newList.removeAt(100); // Delete
newList.insert(4000, {'id': 9999, 'val': 'new'}); // Insert
// Index 2500 shifted to 2499
newList[2499] = {'id': 2500, 'val': 'CHANGED'}; // Modify
final stopwatch = Stopwatch()..start();
final changes = HybridDiffer.diff(oldList, newList, idField: 'id');
stopwatch.stop();
print('Diff 5000 items took: ${stopwatch.elapsedMilliseconds}ms');
final deletes = changes.where((c) => c.op == ChangeOp.delete);
final inserts = changes.where((c) => c.op == ChangeOp.insert);
final modifies = changes.where((c) => c.op == ChangeOp.modify);
expect(deletes.length, equals(1));
expect(inserts.length, equals(1));
expect(modifies.length, equals(1));
// Ensure it's reasonably fast (adjust threshold for your machine)
expect(stopwatch.elapsedMilliseconds, lessThan(500));
});
});
}
@@ -0,0 +1,234 @@
import 'dart:math';
import 'package:diff_algorithims/myers_diff.dart';
import 'package:test/test.dart';
void main() {
final differ = FastJsonDiffer();
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
];
group('FastJsonDiffer Scenarios', () {
test('Default (Deep Equality) detects content changes', () {
// Good for correctness, slower for large lists.
final ops = differ.diff(oldData, newData);
// Filtering out 'equal' ops to check changes
final changes = ops.where((op) => op.type != DiffType.equal).toList();
// Expected changes:
// 1. Delete id:2
// 2. Delete id:3 (old version)
// 3. Insert id:3 (new version)
// 4. Insert id:4
expect(changes.length, equals(4));
expect(
changes.any((op) => op.type == DiffType.delete && op.data['id'] == 2),
isTrue,
);
expect(
changes.any((op) => op.type == DiffType.insert && op.data['id'] == 4),
isTrue,
);
// ID 3 changed, so it should appear as both delete and insert
expect(
changes.any(
(op) =>
op.type == DiffType.delete &&
op.data['id'] == 3 &&
op.data['val'] == 'C',
),
isTrue,
);
expect(
changes.any(
(op) =>
op.type == DiffType.insert &&
op.data['id'] == 3 &&
op.data['val'] == 'Z',
),
isTrue,
);
});
test(
'Optimized (ID + Val Check) behaves like Deep Equality for relevant fields',
() {
// Here we ignore the 'meta' field entirely, but 'val' changed so result is same as above.
final ops = differ.diff(
oldData,
newData,
keyGenerator: (map) => Object.hash(map['id'], map['val']),
);
final changes = ops.where((op) => op.type != DiffType.equal).toList();
expect(changes.length, equals(4));
expect(
changes.any((op) => op.type == DiffType.delete && op.data['id'] == 2),
isTrue,
);
expect(
changes.any((op) => op.type == DiffType.insert && op.data['id'] == 4),
isTrue,
);
// ID 3 changed val
expect(
changes.any((op) => op.type == DiffType.delete && op.data['id'] == 3),
isTrue,
);
expect(
changes.any((op) => op.type == DiffType.insert && op.data['id'] == 3),
isTrue,
);
},
);
test('Ultra Fast (ID Only) detects moves/existence only', () {
// If we only care if the ID exists (e.g. for list animations where content updates later)
final ops = differ.diff(
oldData,
newData,
keyGenerator: (map) => map['id'] as int,
);
final changes = ops.where((op) => op.type != DiffType.equal).toList();
// Expected changes:
// 1. Delete id:2
// 2. Insert id:4
// Node 3 should be EQUAL because ID matches
expect(changes.length, equals(2));
expect(
changes.any((op) => op.type == DiffType.delete && op.data['id'] == 2),
isTrue,
);
expect(
changes.any((op) => op.type == DiffType.insert && op.data['id'] == 4),
isTrue,
);
// Ensure id:3 is considered Equal
final id3Ops = ops.where((op) => op.data['id'] == 3).toList();
expect(id3Ops.length, equals(1));
expect(id3Ops.first.type, equals(DiffType.equal));
});
});
group('Fuzz Tests', () {
test('Randomized Diff/Patch Consistency (500 Iterations)', () {
final r = Random(42);
for (var i = 0; i < 500; i++) {
// 1. Generate two random lists
// We make listB a mutation of listA to ensure some overlap
final listA = _generateRandomList(r, size: r.nextInt(50) + 10);
final listB = _mutateList(r, listA);
// 2. Diff
// Using "Ultra Fast" (ID only) mode since it's cleaner for simple integer maps
// or just standard map equality if we use defaults.
// Let's use standard map equality for robustness.
final ops = differ.diff(listA, listB);
// 3. Reconstruct List B from Ops
final reconstructed = <Map<String, dynamic>>[];
for (final op in ops) {
switch (op.type) {
case DiffType.equal:
case DiffType.insert:
reconstructed.add(op.data);
break;
case DiffType.delete:
// Deletions are skipped in the new list construction
break;
}
}
// 4. Verify
try {
expect(reconstructed, equals(listB));
} catch (e) {
print('Fuzz Failure at iteration $i');
print('List A (len ${listA.length}): $listA');
print('List B (len ${listB.length}): $listB');
print('Ops: $ops');
rethrow;
}
}
});
});
}
// --- Fuzz Utils ---
List<Map<String, dynamic>> _generateRandomList(Random r, {required int size}) {
return List.generate(size, (index) {
return {
'id': r.nextInt(1000), // Random IDs, duplicates possible
'val': r.nextInt(100),
'content': _randomString(r),
};
});
}
List<Map<String, dynamic>> _mutateList(
Random r,
List<Map<String, dynamic>> original,
) {
final clone = List<Map<String, dynamic>>.from(original);
final mutations = r.nextInt(10) + 1; // 1 to 10 mutations
for (var m = 0; m < mutations; m++) {
if (clone.isEmpty) {
clone.add(_generateSingleItem(r));
continue;
}
final action = r.nextInt(3);
if (action == 0) {
// Insert
final index = r.nextInt(clone.length + 1);
clone.insert(index, _generateSingleItem(r));
} else if (action == 1) {
// Delete
final index = r.nextInt(clone.length);
clone.removeAt(index);
} else {
// Modify (Replace item)
final index = r.nextInt(clone.length);
clone[index] = _generateSingleItem(r);
}
}
return clone;
}
Map<String, dynamic> _generateSingleItem(Random r) {
return {
'id': r.nextInt(1000),
'val': r.nextInt(100),
// 'content': _randomString(r), // Keep simple
};
}
String _randomString(Random r) {
const chars = 'abcdefghijklmnopqrstuvwxyz';
return List.generate(5, (_) => chars[r.nextInt(chars.length)]).join();
}
@@ -0,0 +1,316 @@
import 'dart:convert';
import 'dart:math';
import 'package:diff_algorithims/object_transform.dart';
import 'package:test/test.dart';
// =============================================================================
// --- HELPER: PATCH APPLICATOR ---
// =============================================================================
// Since the provided code only included the Generator and UI, we need
// this helper in the test suite to verify the Diff actually works "Round Trip".
Map<String, dynamic> applyPatchHelper(
Map<String, dynamic> original,
Map<String, dynamic> diff,
) {
final clone = Map<String, dynamic>.from(original);
for (final key in diff.keys) {
var diffVal = diff[key];
final originalVal = clone[key];
// Handle Deserialized JSON objects from serializeDiff
if (diffVal is Map && diffVal.containsKey('_op')) {
if (diffVal['_op'] == 'd') {
diffVal = const Deleted();
} else if (diffVal['_op'] == 's') {
diffVal = TextSplice(diffVal['i'], diffVal['d'], diffVal['t']);
}
}
if (diffVal is Deleted) {
clone.remove(key);
} else if (diffVal is TextSplice && originalVal is String) {
clone[key] = diffVal.apply(originalVal);
} else if (diffVal is Map<String, dynamic> &&
originalVal is Map<String, dynamic>) {
clone[key] = applyPatchHelper(originalVal, diffVal);
} else {
clone[key] = diffVal;
}
}
return clone;
}
// =============================================================================
// --- THE TEST SUITE ---
// =============================================================================
void main() {
group('1. TextSplice Logic', () {
test('Correctly splices into the middle of a string', () {
final splice = TextSplice(6, 0, "Dart "); // Insert at index 6
final original = "Hello World";
final result = splice.apply(original);
expect(result, "Hello Dart World");
});
test('Correctly handles deletions', () {
final splice = TextSplice(0, 5, ""); // Delete first 5 chars
final original = "Hello World";
final result = splice.apply(original);
expect(result, " World");
});
test('Correctly handles replacements', () {
// "brown" starts at 10, length 5. Replace with "red".
final splice = TextSplice(10, 5, "red");
final original = "The quick brown fox";
final result = splice.apply(original);
expect(result, "The quick red fox");
});
test('Handles index out of bounds gracefully (Append)', () {
final splice = TextSplice(100, 0, "!");
final original = "Hi";
final result = splice.apply(original);
expect(result, "Hi!");
});
});
group('2. DiffEngine Basic Logic', () {
test('Detects Added Keys', () {
final oldMap = {'a': 1};
final newMap = {'a': 1, 'b': 2};
final diff = DiffEngine.generateDiff(oldMap, newMap);
expect(diff, {'b': 2});
});
test('Detects Deleted Keys', () {
final oldMap = {'a': 1, 'b': 2};
final newMap = {'a': 1};
final diff = DiffEngine.generateDiff(oldMap, newMap);
expect(diff['b'], isA<Deleted>());
});
test('Detects Value Changes (Primitives)', () {
final oldMap = {'a': 1};
final newMap = {'a': 99};
final diff = DiffEngine.generateDiff(oldMap, newMap);
expect(diff, {'a': 99});
});
test('Treats Lists as Atomic Replacements', () {
// The engine is designed to replace lists entirely, not diff indices
final oldMap = {
'tags': ['a', 'b'],
};
final newMap = {
'tags': ['a', 'c'],
};
final diff = DiffEngine.generateDiff(oldMap, newMap);
expect(diff['tags'], ['a', 'c']);
});
test('Recursively Diffs Nested Maps', () {
final oldMap = {
'meta': {'v': 1, 'active': true},
};
final newMap = {
'meta': {'v': 2, 'active': true},
};
final diff = DiffEngine.generateDiff(oldMap, newMap);
expect(diff.containsKey('meta'), true);
expect(diff['meta'], {'v': 2}); // Should NOT contain 'active'
});
});
group('3. Emoji & Unicode Safety (Crucial)', () {
test('Does NOT split a surrogate pair when deleting', () {
// 👋 is \uD83D\uDC4B (2 code units)
final oldMap = {'msg': "Hi 👋"};
final newMap = {'msg': "Hi "};
final diff = DiffEngine.generateDiff(oldMap, newMap);
final splice = diff['msg'] as TextSplice;
// It should delete 2 units (the whole emoji), not 1
expect(splice.deleteCount, 2, reason: "Should delete full emoji");
expect(splice.index, 3);
});
test(
'Handles changing one emoji to another (Surrogate boundary check)',
() {
// 👋 (\uD83D\uDC4B) -> 🤚 (\uD83E\uDD1A)
// Note: They share the high surrogate \uD83... in some encodings,
// or simply look similar. The algorithm must not get confused.
final oldMap = {'msg': "A 👋 B"};
final newMap = {'msg': "A 🤚 B"};
final diff = DiffEngine.generateDiff(oldMap, newMap);
final splice = diff['msg'] as TextSplice;
// Should recognize the change at the emoji
expect(splice.insertText, "🤚");
// Applying it should result in valid string
final patched = applyPatchHelper(oldMap, diff);
expect(patched['msg'], "A 🤚 B");
},
);
test('Handles Complex ZWJ Emojis (Family)', () {
// 👨‍👩‍👧‍👦 is 11 chars long
final family = "👨‍👩‍👧‍👦";
final oldMap = {'icon': "Family: $family"};
final newMap = {'icon': "Family: "}; // Deleted
final diff = DiffEngine.generateDiff(oldMap, newMap);
final splice = diff['icon'] as TextSplice;
// Should delete exactly the length of the emoji + space
// Length of "Family: " is 8.
expect(splice.index, 8);
expect(splice.deleteCount, family.length);
});
});
group('4. Serialization', () {
test('Serializes TextSplice and Deleted correctly', () {
final diff = {
'bio': TextSplice(5, 1, "a"),
'oldField': const Deleted(),
'simple': 123,
};
final jsonStr = serializeDiff(diff);
final decoded = jsonDecode(jsonStr);
expect(decoded['bio']['_op'], 's');
expect(decoded['bio']['i'], 5);
expect(decoded['oldField']['_op'], 'd');
expect(decoded['simple'], 123);
});
test('Round Trip: Serialize -> Deserialize -> Apply', () {
final oldState = {'text': "Hello"};
final diff = {'text': TextSplice(5, 0, " World")};
final jsonStr = serializeDiff(diff);
final decodedDiff = jsonDecode(jsonStr); // Raw JSON Maps
// Our helper must handle the raw JSON maps with _op
final patched = applyPatchHelper(oldState, decodedDiff);
expect(patched['text'], "Hello World");
});
});
group('5. Fuzz Testing (Chaos Monkey)', () {
// This generates random maps, mutates them, calculates diff,
// and verifies that Old + Diff == New.
test('Randomized Property Test (1000 iterations)', () {
final r = Random(42); // Seed for reproducibility
for (int i = 0; i < 1000; i++) {
final stateA = _generateRandomMap(r, depth: 3);
final stateB = _mutateMap(r, stateA);
try {
// 1. Generate
final diff = DiffEngine.generateDiff(stateA, stateB);
// 2. Simulate Network (Serialize/Deserialize)
final jsonStr = serializeDiff(diff);
final decodedDiff = jsonDecode(jsonStr);
// 3. Patch
final reconstructedB = applyPatchHelper(stateA, decodedDiff);
// 4. Verify
final isEqual = jsonEncode(stateB) == jsonEncode(reconstructedB);
if (!isEqual) {
fail('''
Fuzz Failure at iteration $i
Original: $stateA
Target: $stateB
Diff: $decodedDiff
Result: $reconstructedB
''');
}
} catch (e, stack) {
fail('Crash at iteration $i: $e\n$stack');
}
}
});
});
}
// =============================================================================
// --- FUZZER UTILITIES ---
// =============================================================================
Map<String, dynamic> _generateRandomMap(Random r, {int depth = 2}) {
final map = <String, dynamic>{};
final keyCount = r.nextInt(5);
for (int i = 0; i < keyCount; i++) {
final key = 'k$i';
if (depth > 0 && r.nextBool()) {
map[key] = _generateRandomMap(r, depth: depth - 1);
} else {
map[key] = _generateRandomValue(r);
}
}
return map;
}
dynamic _generateRandomValue(Random r) {
final type = r.nextInt(4);
if (type == 0) return r.nextInt(100);
if (type == 1) return r.nextBool();
if (type == 2) {
// Random String with occasional Emoji
if (r.nextBool()) return "Test ${r.nextInt(100)}";
return "Hello 👋 ${r.nextInt(10)}";
}
if (type == 3) return [1, 2, 3]; // Simple list
return null;
}
Map<String, dynamic> _mutateMap(Random r, Map<String, dynamic> original) {
// Deep copy via JSON
final clone = jsonDecode(jsonEncode(original)) as Map<String, dynamic>;
final keys = clone.keys.toList();
// 1. Add new key
if (r.nextBool() || keys.isEmpty) {
clone['new_${r.nextInt(100)}'] = 'added';
return clone; // Return early to keep mutations simple per step
}
// 2. Delete key
if (r.nextBool()) {
clone.remove(keys[r.nextInt(keys.length)]);
return clone;
}
// 3. Modify existing
final key = keys[r.nextInt(keys.length)];
final val = clone[key];
if (val is Map<String, dynamic>) {
clone[key] = _mutateMap(r, val);
} else if (val is String) {
// String mutation
if (val.isNotEmpty) {
clone[key] = val + " appended";
} else {
clone[key] = "New";
}
} else {
clone[key] = "Changed Primitive";
}
return clone;
}