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 _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((ref) { final manager = ref.watch(syncManagerProvider); final observer = SyncObserver(manager); // External, non-invasive observation of the frozen RideStateMachine ref.listen( rideStateMachineProvider, (previous, next) => observer.onRideSessionChanged(previous, next), ); return observer; });