77 lines
3.1 KiB
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;
|
|
});
|