301 lines
9.2 KiB
Dart
301 lines
9.2 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,
|
|
|
|
/// Bundle exists in cloud (e.g. historical ride) but lacks modern cryptographic checksum verification.
|
|
legacyUnverified,
|
|
}
|
|
|
|
/// 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,
|
|
);
|
|
|
|
const CloudConfirmationResult.legacyUnverified({
|
|
required String remoteRideId,
|
|
String? ownerUid,
|
|
String? objectKey,
|
|
int? sizeBytes,
|
|
String? sha256,
|
|
String? message,
|
|
}) : this(
|
|
status: CloudConfirmationStatus.legacyUnverified,
|
|
remoteRideId: remoteRideId,
|
|
ownerUid: ownerUid,
|
|
objectKey: objectKey,
|
|
sizeBytes: sizeBytes,
|
|
sha256: sha256,
|
|
message: message,
|
|
);
|
|
|
|
bool get isConfirmed => status == CloudConfirmationStatus.confirmed;
|
|
bool get isLegacyUnverified =>
|
|
status == CloudConfirmationStatus.legacyUnverified;
|
|
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 || result.isLegacyUnverified;
|
|
}
|
|
|
|
/// 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 || result.isLegacyUnverified;
|
|
}
|
|
|
|
@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;
|
|
|
|
final existingRemoteId = item.remoteId;
|
|
if (existingRemoteId != null) {
|
|
final confirmation = await verifyCloudConfirmation(existingRemoteId);
|
|
if (confirmation.isConfirmed || confirmation.isLegacyUnverified) {
|
|
return SyncResult.success(remoteId: existingRemoteId);
|
|
}
|
|
if (confirmation.isInconsistent) {
|
|
return SyncResult.permanentFailure(
|
|
'Cloud data inconsistent: ${confirmation.message}',
|
|
);
|
|
}
|
|
}
|
|
|
|
switch (behavior) {
|
|
case FakeSyncBehavior.success:
|
|
uploadedItems.add(item);
|
|
final remoteId =
|
|
existingRemoteId ??
|
|
'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();
|
|
}
|
|
}
|