151 lines
4.9 KiB
Dart
151 lines
4.9 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:relationship_saver/core/config/app_config.dart';
|
|
import 'package:relationship_saver/features/sync/storage/sync_state_store.dart';
|
|
import 'package:relationship_saver/features/sync/storage/sync_state_store_hive.dart';
|
|
import 'package:relationship_saver/features/sync/storage/sync_state_store_provider.dart';
|
|
import 'package:relationship_saver/features/sync/storage/sync_state_store_shared_prefs.dart';
|
|
import 'package:relationship_saver/features/sync/sync_state.dart';
|
|
import 'package:relationship_saver/integrations/backend/models/backend_models.dart';
|
|
|
|
/// Persists queued local mutations and sync cursor metadata.
|
|
class SyncQueueRepository extends AsyncNotifier<SyncState> {
|
|
@override
|
|
Future<SyncState> build() async {
|
|
final SyncStateStore store = ref.read(syncStateStoreProvider);
|
|
SyncStateRecord? record = await store.read();
|
|
|
|
if ((record == null || record.rawState.isEmpty) &&
|
|
AppConfig.useHiveLocalDb &&
|
|
store is HiveSyncStateStore) {
|
|
final SharedPrefsSyncStateStore legacyStore = SharedPrefsSyncStateStore();
|
|
final SyncStateRecord? legacy = await legacyStore.read();
|
|
if (legacy != null && legacy.rawState.isNotEmpty) {
|
|
await store.write(rawState: legacy.rawState);
|
|
await legacyStore.clear();
|
|
record = await store.read();
|
|
}
|
|
}
|
|
|
|
if (record == null || record.rawState.isEmpty) {
|
|
return SyncState.empty;
|
|
}
|
|
|
|
try {
|
|
final Map<String, dynamic> json =
|
|
jsonDecode(record.rawState) as Map<String, dynamic>;
|
|
return SyncState.fromJson(json);
|
|
} on FormatException {
|
|
await store.clear();
|
|
return SyncState.empty;
|
|
}
|
|
}
|
|
|
|
Future<void> enqueue(ChangeEnvelope change) async {
|
|
final SyncState current = await _currentState();
|
|
final List<ChangeEnvelope> pending = <ChangeEnvelope>[
|
|
change,
|
|
...current.pendingChanges,
|
|
];
|
|
await _setState(current.copyWith(pendingChanges: pending));
|
|
}
|
|
|
|
Future<void> enqueueAll(Iterable<ChangeEnvelope> changes) async {
|
|
final List<ChangeEnvelope> values = changes.toList(growable: false);
|
|
if (values.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
final SyncState current = await _currentState();
|
|
final List<ChangeEnvelope> pending = <ChangeEnvelope>[
|
|
...values,
|
|
...current.pendingChanges,
|
|
];
|
|
await _setState(current.copyWith(pendingChanges: pending));
|
|
}
|
|
|
|
Future<void> applyPushResult(SyncPushResult result, {DateTime? at}) async {
|
|
final SyncState current = await _currentState();
|
|
final DateTime now = at ?? DateTime.now();
|
|
final Set<String> completedMutationIds = <String>{
|
|
...result.accepted.map((MutationAck ack) => ack.clientMutationId),
|
|
...result.rejected.map(
|
|
(MutationRejection rejection) => rejection.clientMutationId,
|
|
),
|
|
};
|
|
final List<ChangeEnvelope> 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<void> 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 <MutationRejection>[],
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> 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<void> clearQueue() async {
|
|
final SyncState current = await _currentState();
|
|
await _setState(
|
|
current.copyWith(
|
|
pendingChanges: const <ChangeEnvelope>[],
|
|
lastRejected: const <MutationRejection>[],
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<SyncState> _currentState() async {
|
|
final SyncState? value = state.asData?.value;
|
|
if (value != null) {
|
|
return value;
|
|
}
|
|
return future;
|
|
}
|
|
|
|
Future<void> _setState(SyncState next) async {
|
|
state = AsyncData<SyncState>(next);
|
|
await ref
|
|
.read(syncStateStoreProvider)
|
|
.write(rawState: jsonEncode(next.toJson()));
|
|
}
|
|
}
|
|
|
|
final AsyncNotifierProvider<SyncQueueRepository, SyncState>
|
|
syncQueueRepositoryProvider =
|
|
AsyncNotifierProvider<SyncQueueRepository, SyncState>(
|
|
SyncQueueRepository.new,
|
|
);
|