import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:relationship_saver/features/sync/sync_state.dart'; import 'package:relationship_saver/integrations/backend/models/backend_models.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// Persists queued local mutations and sync cursor metadata. class SyncQueueRepository extends AsyncNotifier { static const String _storageKey = 'sync_queue_state_v1'; @override Future build() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); final String? raw = prefs.getString(_storageKey); if (raw == null || raw.isEmpty) { return SyncState.empty; } try { final Map json = jsonDecode(raw) as Map; return SyncState.fromJson(json); } on FormatException { await prefs.remove(_storageKey); return SyncState.empty; } } Future enqueue(ChangeEnvelope change) async { final SyncState current = await _currentState(); final List pending = [ change, ...current.pendingChanges, ]; await _setState(current.copyWith(pendingChanges: pending)); } Future enqueueAll(Iterable changes) async { final List values = changes.toList(growable: false); if (values.isEmpty) { return; } final SyncState current = await _currentState(); final List pending = [ ...values, ...current.pendingChanges, ]; await _setState(current.copyWith(pendingChanges: pending)); } Future applyPushResult(SyncPushResult result, {DateTime? at}) async { final SyncState current = await _currentState(); final DateTime now = at ?? DateTime.now(); final Set completedMutationIds = { ...result.accepted.map((MutationAck ack) => ack.clientMutationId), ...result.rejected.map( (MutationRejection rejection) => rejection.clientMutationId, ), }; final List pending = current.pendingChanges .where( (ChangeEnvelope change) => !completedMutationIds.contains(change.clientMutationId), ) .toList(growable: false); final String? rejectionMessage = result.rejected.isEmpty ? null : 'Push rejected ${result.rejected.length} change(s).'; await _setState( current.copyWith( cursor: result.cursor, pendingChanges: pending, lastAttemptAt: now, lastSyncAt: now, lastError: rejectionMessage, lastRejected: result.rejected, ), ); } Future applyPullResult(SyncPullResult result, {DateTime? at}) async { final SyncState current = await _currentState(); final DateTime now = at ?? DateTime.now(); await _setState( current.copyWith( cursor: result.cursor, lastAttemptAt: now, lastSyncAt: now, lastError: null, lastRejected: const [], ), ); } Future markFailure(Object error, {DateTime? at}) async { final SyncState current = await _currentState(); final DateTime now = at ?? DateTime.now(); await _setState(current.copyWith(lastAttemptAt: now, lastError: '$error')); } Future clearQueue() async { final SyncState current = await _currentState(); await _setState( current.copyWith( pendingChanges: const [], lastRejected: const [], ), ); } Future _currentState() async { final SyncState? value = state.asData?.value; if (value != null) { return value; } return future; } Future _setState(SyncState next) async { state = AsyncData(next); final SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setString(_storageKey, jsonEncode(next.toJson())); } } final AsyncNotifierProvider syncQueueRepositoryProvider = AsyncNotifierProvider( SyncQueueRepository.new, );