MotorSignal-App/lib/services/sync/sync_provider.dart

113 lines
3.3 KiB
Dart

import 'dart:async';
import 'sync_models.dart';
/// Abstract contract for synchronization providers (e.g. Firebase, GitHub, Supabase).
/// Implementations must not leak provider-specific SDK types into the core sync engine.
abstract class SyncProvider {
/// Unique identifier of the provider (e.g. 'firebase', 'github', 'fake')
String get providerId;
/// Whether the provider backend and network are currently available
Future<bool> isAvailable();
/// Whether this provider requires an active user authentication credential
Future<bool> requiresAuthentication();
/// Performs the upload of the local ride referenced by [item].
/// Must return a deterministic [SyncResult].
Future<SyncResult> uploadRide(SyncItem item);
/// Checks if the ride with [remoteRideId] is already confirmed uploaded in the cloud.
/// Default implementation returns false.
Future<bool> isRideConfirmedInCloud(String remoteRideId) async => false;
}
/// Simulated behavior modes for testing the synchronization engine deterministically
enum FakeSyncBehavior {
success,
temporaryFailure,
permanentFailure,
authRequired,
offline,
}
/// Deterministic, in-memory fake synchronization provider for testing
class FakeSyncProvider implements SyncProvider {
@override
final String providerId;
FakeSyncBehavior behavior;
int delayMs;
final List<SyncItem> uploadedItems = [];
final Map<int, int> uploadAttemptsPerRide = {};
final Set<String> confirmedRemoteRideIds = {};
FakeSyncProvider({
this.providerId = 'fake',
this.behavior = FakeSyncBehavior.success,
this.delayMs = 0,
});
@override
Future<bool> isAvailable() async {
if (behavior == FakeSyncBehavior.offline) return false;
return true;
}
@override
Future<bool> requiresAuthentication() async {
return behavior == FakeSyncBehavior.authRequired;
}
@override
Future<bool> isRideConfirmedInCloud(String remoteRideId) async {
return confirmedRemoteRideIds.contains(remoteRideId);
}
@override
Future<SyncResult> uploadRide(SyncItem item) async {
if (delayMs > 0) {
await Future<void>.delayed(Duration(milliseconds: delayMs));
}
uploadAttemptsPerRide[item.localRideId] =
(uploadAttemptsPerRide[item.localRideId] ?? 0) + 1;
switch (behavior) {
case FakeSyncBehavior.success:
uploadedItems.add(item);
final remoteId = 'remote_${item.localRideId}_${DateTime.now().millisecondsSinceEpoch}';
confirmedRemoteRideIds.add(remoteId);
return SyncResult.success(
remoteId: remoteId,
);
case FakeSyncBehavior.temporaryFailure:
return SyncResult.temporaryFailure(
'Simulated temporary network/server error (HTTP 503)',
);
case FakeSyncBehavior.permanentFailure:
return SyncResult.permanentFailure(
'Simulated unrecoverable payload or validation error (HTTP 422)',
);
case FakeSyncBehavior.authRequired:
return SyncResult.authRequired(
message: 'Simulated authentication expired or required (HTTP 401)',
);
case FakeSyncBehavior.offline:
return SyncResult.temporaryFailure(
'Simulated offline network unreachable',
);
}
}
void reset() {
uploadedItems.clear();
uploadAttemptsPerRide.clear();
confirmedRemoteRideIds.clear();
}
}