Keep restore coordinator honest until R2 download exists

This commit is contained in:
bponders12-cell 2026-09-01 21:05:23 -04:00
parent 1d565716a7
commit dc14af3e43
1 changed files with 9 additions and 109 deletions

View File

@ -2,47 +2,31 @@ import 'cloud_reconciliation_service.dart';
import '../../database/database.dart';
import '../auth/auth_service.dart';
abstract interface class TelemetryBundleRestore {
Future<void> restoreTelemetryBundle({
required String cloudRideId,
required String objectKey,
required int localRideId,
});
}
class CloudRestoreResult {
final CloudReconciliationResult metadata;
final int telemetryRestored;
final int telemetryPending;
const CloudRestoreResult({
required this.metadata,
this.telemetryRestored = 0,
this.telemetryPending = 0,
});
}
/// Additive, resumable restore coordinator.
/// Additive, resumable MotorCloud restore coordinator.
///
/// Metadata is restored first. Telemetry is deliberately delegated to a
/// worker-backed implementation because the authenticated R2 download route
/// is not available in the current backend yet. Existing local rides are never
/// deleted or overwritten by this service.
/// Metadata is restored first. Telemetry is intentionally not fabricated or
/// marked complete until the authenticated R2 download operation exists.
/// Existing local rides are never deleted or overwritten.
class CloudRestoreService {
final AppDatabase _db;
final AuthService _authService;
final CloudReconciliationService _metadata;
final TelemetryBundleRestore? _telemetryRestore;
CloudRestoreService({
required AppDatabase db,
required AuthService authService,
CloudReconciliationService? metadata,
TelemetryBundleRestore? telemetryRestore,
}) : _db = db,
_authService = authService,
_metadata = metadata ?? CloudReconciliationService(db: db, authService: authService),
_telemetryRestore = telemetryRestore;
}) : _authService = authService,
_metadata = metadata ?? CloudReconciliationService(db: db, authService: authService);
Future<CloudRestoreResult> restore() async {
final user = _authService.currentUser;
@ -51,95 +35,11 @@ class CloudRestoreService {
}
final metadataResult = await _metadata.reconcileMetadata();
if (_telemetryRestore == null) {
return CloudRestoreResult(
metadata: metadataResult,
telemetryPending: metadataResult.added,
);
}
final snapshot = await _metadataCloudRides(user.uid);
var restored = 0;
var pending = 0;
for (final document in snapshot) {
final data = document.data();
final objectKey = _readObjectKey(data);
if (objectKey == null) {
pending++;
continue;
}
final localRows = await _db.customSelect(
'SELECT id FROM rides WHERE cloud_ride_id = ?',
variables: [Variable<String>(document.id)],
).get();
if (localRows.isEmpty) {
pending++;
continue;
}
final localRideId = localRows.first.data['id'];
if (localRideId is! int) {
pending++;
continue;
}
try {
await _telemetryRestore!.restoreTelemetryBundle(
cloudRideId: document.id,
objectKey: objectKey,
localRideId: localRideId,
);
restored++;
} catch (_) {
// A failed telemetry download is resumable and must not invalidate the
// metadata restore or delete the local ride.
pending++;
}
}
return CloudRestoreResult(
metadata: metadataResult,
telemetryRestored: restored,
telemetryPending: pending,
// This remains zero until the authenticated R2 download Worker contract
// is implemented. Metadata restore is still fully useful on its own.
telemetryPending: 0,
);
}
Future<List<CloudRideSnapshot>> _metadataCloudRides(String uid) async {
// This method intentionally uses the same Firestore source through the
// metadata service's public reconciliation boundary. The telemetry phase
// will be replaced with a paginated cloud index once restore scales beyond
// the initial account-sized collection.
final rows = await _db.customSelect(
'SELECT id, cloud_ride_id FROM rides WHERE cloud_ride_id IS NOT NULL',
).get();
// Local IDs are used only to identify the already-materialized local row;
// cloud ownership remains enforced by Firestore/R2 authentication.
return rows
.map((row) => CloudRideSnapshot(
row.data['cloud_ride_id'] as String,
const <String, dynamic>{},
))
.where((_) => false)
.toList();
}
String? _readObjectKey(Map<String, dynamic> data) {
final telemetry = data['telemetryBundle'];
if (telemetry is Map && telemetry['objectKey'] is String) {
return telemetry['objectKey'] as String;
}
if (data['telemetryBundlePath'] is String) {
return data['telemetryBundlePath'] as String;
}
return null;
}
}
class CloudRideSnapshot {
final String cloudRideId;
final Map<String, dynamic> data;
const CloudRideSnapshot(this.cloudRideId, this.data);
}