Add background sync runner and rejected-sync UX

This commit is contained in:
Rijad Zuzo
2026-02-15 20:13:12 +01:00
parent add0105e6b
commit 497805ed3b
8 changed files with 361 additions and 2 deletions
@@ -0,0 +1,65 @@
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:relationship_saver/features/sync/sync_auto_trigger_controller.dart';
import 'package:relationship_saver/features/sync/sync_coordinator.dart';
void main() {
test('triggers sync once and respects cooldown interval', () async {
int callCount = 0;
DateTime now = DateTime(2026, 2, 15, 12);
final SyncAutoTriggerController controller = SyncAutoTriggerController(
syncNow: () async {
callCount += 1;
return _result(success: true);
},
now: () => now,
minimumInterval: const Duration(seconds: 60),
);
final bool first = await controller.trigger();
final bool second = await controller.trigger();
now = now.add(const Duration(seconds: 61));
final bool third = await controller.trigger();
expect(first, isTrue);
expect(second, isFalse);
expect(third, isTrue);
expect(callCount, 2);
expect(controller.lastTriggeredAt, now);
});
test('does not run a second sync while a run is still in progress', () async {
int callCount = 0;
final Completer<void> completer = Completer<void>();
final SyncAutoTriggerController controller = SyncAutoTriggerController(
syncNow: () async {
callCount += 1;
await completer.future;
return _result(success: true);
},
now: DateTime.now,
minimumInterval: Duration.zero,
);
final Future<bool> first = controller.trigger();
final bool second = await controller.trigger();
completer.complete();
final bool firstCompleted = await first;
expect(firstCompleted, isTrue);
expect(second, isFalse);
expect(callCount, 1);
});
}
SyncRunResult _result({required bool success}) {
return SyncRunResult(
success: success,
operation: 'sync',
message: success ? 'ok' : 'fail',
pendingAfter: 0,
);
}
@@ -70,6 +70,43 @@ void main() {
expect(state.lastError, contains('Push rejected'));
},
);
test(
'clearRejections removes last rejected payload and rejection error',
() async {
final ProviderContainer container = ProviderContainer(
overrides: [
syncStateStoreProvider.overrideWithValue(InMemorySyncStateStore()),
],
);
addTearDown(container.dispose);
final SyncQueueRepository notifier = container.read(
syncQueueRepositoryProvider.notifier,
);
await notifier.enqueue(_change(id: 'p-1', mutationId: 'cm-r'));
await notifier.applyPushResult(
SyncPushResult(
cursor: 'cursor-3',
accepted: const <MutationAck>[],
rejected: const <MutationRejection>[
MutationRejection(
clientMutationId: 'cm-r',
code: 'VALIDATION_FAILED',
message: 'invalid payload',
),
],
),
at: DateTime(2026, 2, 15, 13),
);
await notifier.clearRejections();
final SyncStateData state = await _readState(container);
expect(state.rejected, isEmpty);
expect(state.lastError, isNull);
},
);
}
class SyncStateData {