projectionValuesEqual took a Uint8List (how a bytea column arrives) down its List branch and compared it byte by byte, and tryUuidValue parsed every value as a UUID by throwing and catching an error when it was none. Each byte of a payload thus cost a thrown TypeError, about 18 µs: two payloads of 2 KB took 36 ms to compare. A sync whose projection loaded a few thousand rows with payloads kept the server's isolate busy for minutes, during which it answered no other request and the client's method stream timed out, so the client sent the same changes again. Byte values are now compared as bytes, and tryUuidValue only parses what can hold a UUID (a UuidValue, 16 bytes, a 36-character string), returning null for anything else without throwing. A UUID compares with its string and its bytes as before. Longer byte values are no longer read as the UUID of their first 16 bytes, which made canonicalProjectionValue group payloads with the same beginning together. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
90 lines
3 KiB
Dart
90 lines
3 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:serverpod_offline_sync/src/database/merge_utils/unique_resolver.dart';
|
|
import 'package:test/test.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
void main() {
|
|
group('Given byte values such as a bytea column holds,', () {
|
|
final bytes = Uint8List.fromList(List.generate(4096, (i) => i % 251));
|
|
|
|
test(
|
|
'when comparing equal and differing bytes, '
|
|
'then they compare by content.',
|
|
() {
|
|
final same = Uint8List.fromList(bytes);
|
|
final changed = Uint8List.fromList(bytes)..[4000] ^= 1;
|
|
final shorter = Uint8List.sublistView(bytes, 0, 4095);
|
|
|
|
expect(projectionValuesEqual(bytes, same), isTrue);
|
|
expect(projectionValuesEqual(bytes, changed), isFalse);
|
|
expect(projectionValuesEqual(bytes, shorter), isFalse);
|
|
expect(projectionValuesEqual(bytes, null), isFalse);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'when comparing payloads alike in their first 16 bytes, '
|
|
'then they are not taken for the same UUID.',
|
|
() {
|
|
final payload = Uint8List.fromList('{"schemaVersion":1,"a":1}'.codeUnits);
|
|
final other = Uint8List.fromList('{"schemaVersion":1,"a":2}'.codeUnits);
|
|
|
|
expect(tryUuidValue(payload), isNull);
|
|
expect(
|
|
canonicalProjectionValue(payload),
|
|
isNot(canonicalProjectionValue(other)),
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'when comparing many payloads of a few kilobytes, '
|
|
'then it takes no more than a moment.',
|
|
() {
|
|
final same = Uint8List.fromList(bytes);
|
|
final watch = Stopwatch()..start();
|
|
for (var i = 0; i < 1000; i++) {
|
|
projectionValuesEqual(bytes, same);
|
|
}
|
|
|
|
// Byte by byte through a failing UUID parse, this took minutes.
|
|
expect(watch.elapsed, lessThan(const Duration(seconds: 2)));
|
|
},
|
|
);
|
|
});
|
|
|
|
group('Given UUIDs in their forms,', () {
|
|
final uuid = const Uuid().v7obj();
|
|
|
|
test(
|
|
'when comparing a UUID with its string and its bytes, '
|
|
'then they are the same.',
|
|
() {
|
|
expect(tryUuidValue(uuid.uuid), uuid);
|
|
expect(tryUuidValue(Uint8List.fromList(uuid.toBytes())), uuid);
|
|
expect(projectionValuesEqual(uuid, uuid.uuid), isTrue);
|
|
expect(projectionValuesEqual(uuid.uuid, uuid), isTrue);
|
|
expect(
|
|
projectionValuesEqual(Uint8List.fromList(uuid.toBytes()), uuid),
|
|
isTrue,
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'when comparing what is no UUID, '
|
|
'then it is none and compares as itself.',
|
|
() {
|
|
expect(tryUuidValue('not a uuid'), isNull);
|
|
expect(tryUuidValue('x' * 36), isNull);
|
|
expect(tryUuidValue(42), isNull);
|
|
expect(tryUuidValue(DateTime.utc(2026)), isNull);
|
|
expect(projectionValuesEqual(42, 42), isTrue);
|
|
expect(projectionValuesEqual(42, 43), isFalse);
|
|
expect(projectionValuesEqual([1, 'a'], [1, 'a']), isTrue);
|
|
expect(projectionValuesEqual({'a': 1}, {'a': 2}), isFalse);
|
|
},
|
|
);
|
|
});
|
|
}
|