/// Tests for the revision tracking that keeps sync inside the free tier. /// /// Two savings, both measured against the GitHub-backed history this /// replaces: 88.3% of pushes there were byte-identical no-ops, and every tick /// re-downloaded every peer's whole log regardless of whether it had changed. /// These tests assert the *request counts*, since that -- not the merge /// result -- is what the free-tier headroom depends on. library; import 'dart:convert'; import 'package:crdt_sync/crdt_sync.dart'; import 'package:test/test.dart'; Log _log(String id, String value, {String nodeId = 'node-a'}) => { id: Record( id: id, fields: { 'value': (value, Hlc(wallTimeMs: 1000, counter: 0, nodeId: nodeId)), }, ), }; String _encode(Log log) => jsonEncode(log.map((id, record) => MapEntry(id, record.toJson()))); Log _decode(String text) => (jsonDecode(text) as Map).map( (id, data) => MapEntry(id, Record.fromJson(data as Map)), ); /// An in-memory [RemoteStore] that also serves revision maps in one read, /// standing in for [FirebaseRestClient]. Counts every call so tests can /// assert on traffic rather than on results. class _FakeRemote implements RemoteStore, BulkMapReader { _FakeRemote(this.files); final Map files; final List reads = []; final List writes = []; final List mapReads = []; @override Future> listDirectory(String path) async => files.keys .where((key) => key.startsWith('$path/')) .map((key) => key.substring(path.length + 1).split('/').first) .toSet() .toList(); @override Future getFileText(String path) async { reads.add(path); return files[path]; } @override Future putFileText( String path, String text, { required String message, }) async { writes.add(path); files[path] = text; } @override Future> getStringMap(String path) async { mapReads.add(path); return { for (final entry in files.entries) if (entry.key.startsWith('$path/')) entry.key.substring(path.length + 1): entry.value, }; } @override Future deleteFile(String path, {String message = ''}) async => files.remove(path); @override Future canAccessRemote() async => true; @override void close() {} } Future _tick( RemoteStore remote, SyncStateStore? store, { Log local = const {}, String deviceId = 'pc', }) => syncLog( client: remote, deviceId: deviceId, pathPrefix: 'ns/devices', localLog: local, encode: _encode, decode: _decode, stateStore: store, ); void main() { group('revision publishing order', () { test('publishes the log before its revision', () async { // Reversed, a peer would cache "seen rev X" against a log it never // received, and skip it forever. final remote = _FakeRemote({}); await _tick(remote, InMemorySyncStateStore(), local: _log('a', '1')); expect( remote.writes.indexOf('ns/devices/pc/log.json'), lessThan(remote.writes.indexOf('ns/revs/pc')), ); }); test('each device writes only its own revision key', () async { // Per-device keys rather than one shared map: a whole-map write would // erase every other device's entry, after which those peers would look // permanently unchanged and never be fetched again. final remote = _FakeRemote({'ns/revs/phone': 'peer-rev'}); await _tick(remote, InMemorySyncStateStore(), local: _log('a', '1')); expect(remote.files['ns/revs/phone'], 'peer-rev'); }); }); group('PersistedSyncStateStore', () {}); }