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

77 lines
3.1 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../features/rides/ride_state_machine.dart';
import 'sync_manager.dart';
import 'sync_providers.dart';
/// External observer listening for ride state machine transitions.
/// Automatically enqueues completed rides into the synchronization pipeline
/// without modifying or blocking the frozen RideStateMachine.
class SyncObserver {
final SyncManager _syncManager;
int? _lastEnqueuedRideId;
SyncObserver(this._syncManager);
/// Called when the RideStateMachine state changes in Riverpod
void onRideSessionChanged(RideSession? previous, RideSession next) {
// Detect transition into finished state
final wasFinished = previous?.state == RideState.finished;
final isFinished = next.state == RideState.finished;
final completedRideId = next.dbId;
if (!wasFinished && isFinished && completedRideId != null) {
if (_lastEnqueuedRideId != completedRideId) {
_lastEnqueuedRideId = completedRideId;
debugPrint('[SyncObserver] Ride #$completedRideId completed, enqueuing to SyncManager');
// RID-1: the enqueue Future is now handled explicitly instead of being
// left naked. SyncManager.enqueueRide can throw (SyncOutbox raises
// StateError at capacity and ArgumentError on invalid input), and an
// unhandled async exception here silently discarded the completed
// ride's durable sync intent with no diagnostic.
//
// _enqueueCompletedRide is still invoked synchronously at this point,
// so the callee's synchronous prefix runs before this method returns
// and the outbox's in-memory mutation stays observable to callers.
unawaited(_enqueueCompletedRide(completedRideId));
}
}
}
/// Awaits the enqueue and reports failure instead of swallowing it.
///
/// Diagnostics intentionally carry only the exception's runtime type, never
/// its message or payload, so tokens, UIDs and ride content cannot leak into
/// logs. Output is bounded to at most one line per completed ride because
/// [_lastEnqueuedRideId] suppresses repeat notifications for the same ride.
///
/// Durability note: if this enqueue fails the automatic sync intent for that
/// ride is lost. No retry or reconciliation subsystem is added here by
/// design; see the RID-1 report for the documented follow-up risk.
Future<void> _enqueueCompletedRide(int rideId) async {
try {
await _syncManager.enqueueRide(rideId);
} catch (error) {
debugPrint(
'[SyncObserver] Enqueue failed for ride #$rideId class=${error.runtimeType}',
);
}
}
}
/// Riverpod provider establishing the active SyncObserver listener
final syncObserverProvider = Provider<SyncObserver>((ref) {
final manager = ref.watch(syncManagerProvider);
final observer = SyncObserver(manager);
// External, non-invasive observation of the frozen RideStateMachine
ref.listen<RideSession>(
rideStateMachineProvider,
(previous, next) => observer.onRideSessionChanged(previous, next),
);
return observer;
});