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

265 lines
8.0 KiB
Dart

import 'dart:async';
import 'sync_models.dart';
/// Explicit cloud confirmation states representing the exact verification status of a remote ride.
enum CloudConfirmationStatus {
/// Firestore metadata and corresponding R2 bundle are consistently verified.
confirmed,
/// Required Firestore metadata or R2 object is absent.
missing,
/// Ride identity, owner, bundle reference, size, or checksum does not match.
inconsistent,
/// The current user cannot access or verify the ride.
unauthorized,
/// Network outage, timeout, rate limit, or transient backend failure.
temporarilyUnverifiable,
}
/// Explicit verification report for a cloud ride.
class CloudConfirmationResult {
final CloudConfirmationStatus status;
final String? message;
final String? remoteRideId;
final String? ownerUid;
final String? objectKey;
final int? sizeBytes;
final String? sha256;
const CloudConfirmationResult({
required this.status,
this.message,
this.remoteRideId,
this.ownerUid,
this.objectKey,
this.sizeBytes,
this.sha256,
});
const CloudConfirmationResult.confirmed({
required String remoteRideId,
String? ownerUid,
String? objectKey,
int? sizeBytes,
String? sha256,
String? message,
}) : this(
status: CloudConfirmationStatus.confirmed,
remoteRideId: remoteRideId,
ownerUid: ownerUid,
objectKey: objectKey,
sizeBytes: sizeBytes,
sha256: sha256,
message: message,
);
const CloudConfirmationResult.missing({
required String remoteRideId,
String? message,
}) : this(
status: CloudConfirmationStatus.missing,
remoteRideId: remoteRideId,
message: message,
);
const CloudConfirmationResult.inconsistent({
required String remoteRideId,
String? ownerUid,
String? objectKey,
String? message,
}) : this(
status: CloudConfirmationStatus.inconsistent,
remoteRideId: remoteRideId,
ownerUid: ownerUid,
objectKey: objectKey,
message: message,
);
const CloudConfirmationResult.unauthorized({
required String remoteRideId,
String? ownerUid,
String? message,
}) : this(
status: CloudConfirmationStatus.unauthorized,
remoteRideId: remoteRideId,
ownerUid: ownerUid,
message: message,
);
const CloudConfirmationResult.temporarilyUnverifiable({
required String remoteRideId,
String? message,
}) : this(
status: CloudConfirmationStatus.temporarilyUnverifiable,
remoteRideId: remoteRideId,
message: message,
);
bool get isConfirmed => status == CloudConfirmationStatus.confirmed;
bool get isMissing => status == CloudConfirmationStatus.missing;
bool get isInconsistent => status == CloudConfirmationStatus.inconsistent;
bool get isUnauthorized => status == CloudConfirmationStatus.unauthorized;
bool get isTemporarilyUnverifiable =>
status == CloudConfirmationStatus.temporarilyUnverifiable;
@override
String toString() =>
'CloudConfirmationResult(status: $status, ride: $remoteRideId, owner: $ownerUid, message: $message)';
}
/// 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.
/// Backward compatible wrapper around [verifyCloudConfirmation].
Future<bool> isRideConfirmedInCloud(String remoteRideId) async {
final result = await verifyCloudConfirmation(remoteRideId);
return result.isConfirmed;
}
/// Deeply verifies whether [remoteRideId] is confirmed and recoverable in the cloud.
/// Default implementation returns [CloudConfirmationStatus.missing].
Future<CloudConfirmationResult> verifyCloudConfirmation(
String remoteRideId,
) async => CloudConfirmationResult.missing(remoteRideId: remoteRideId);
}
/// 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 = {};
final Map<String, CloudConfirmationResult> confirmationResults = {};
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 {
final result = await verifyCloudConfirmation(remoteRideId);
return result.isConfirmed;
}
@override
Future<CloudConfirmationResult> verifyCloudConfirmation(
String remoteRideId,
) async {
if (confirmationResults.containsKey(remoteRideId)) {
return confirmationResults[remoteRideId]!;
}
if (confirmedRemoteRideIds.contains(remoteRideId)) {
return CloudConfirmationResult.confirmed(remoteRideId: remoteRideId);
}
if (behavior == FakeSyncBehavior.offline ||
behavior == FakeSyncBehavior.temporaryFailure) {
return CloudConfirmationResult.temporarilyUnverifiable(
remoteRideId: remoteRideId,
message: 'Fake provider backend unavailable',
);
}
if (behavior == FakeSyncBehavior.authRequired) {
return CloudConfirmationResult.unauthorized(
remoteRideId: remoteRideId,
message: 'Fake provider requires authentication',
);
}
return CloudConfirmationResult.missing(
remoteRideId: remoteRideId,
message: 'Ride not found in fake provider',
);
}
@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);
confirmationResults[remoteId] = CloudConfirmationResult.confirmed(
remoteRideId: 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();
confirmationResults.clear();
}
}