Migrate sync queue persistence to store adapters

This commit is contained in:
Rijad Zuzo
2026-02-15 19:38:46 +01:00
parent 1502360051
commit add0105e6b
13 changed files with 293 additions and 18 deletions
@@ -0,0 +1,18 @@
/// Raw persisted payload for sync queue state.
class SyncStateRecord {
const SyncStateRecord({required this.rawState});
final String rawState;
}
/// Persistence boundary for sync queue metadata and pending changes.
abstract interface class SyncStateStore {
/// Reads stored sync state payload.
Future<SyncStateRecord?> read();
/// Writes sync state payload.
Future<void> write({required String rawState});
/// Clears stored sync state payload.
Future<void> clear();
}
@@ -0,0 +1,46 @@
import 'package:hive_flutter/hive_flutter.dart';
import 'package:relationship_saver/features/sync/storage/sync_state_store.dart';
/// Hive-backed sync state store.
class HiveSyncStateStore implements SyncStateStore {
static const String _boxName = 'relationship_saver_sync';
static const String _stateKey = 'sync_state_json';
static bool _initialized = false;
@override
Future<void> clear() async {
final Box<dynamic> box = await _openBox();
await box.delete(_stateKey);
}
@override
Future<SyncStateRecord?> read() async {
final Box<dynamic> box = await _openBox();
final String? rawState = box.get(_stateKey) as String?;
if (rawState == null || rawState.isEmpty) {
return null;
}
return SyncStateRecord(rawState: rawState);
}
@override
Future<void> write({required String rawState}) async {
final Box<dynamic> box = await _openBox();
await box.put(_stateKey, rawState);
}
Future<Box<dynamic>> _openBox() async {
if (!_initialized) {
await Hive.initFlutter();
_initialized = true;
}
if (!Hive.isBoxOpen(_boxName)) {
return Hive.openBox<dynamic>(_boxName);
}
return Hive.box<dynamic>(_boxName);
}
}
@@ -0,0 +1,19 @@
import 'package:relationship_saver/features/sync/storage/sync_state_store.dart';
/// In-memory sync state store for tests.
class InMemorySyncStateStore implements SyncStateStore {
SyncStateRecord? _record;
@override
Future<void> clear() async {
_record = null;
}
@override
Future<SyncStateRecord?> read() async => _record;
@override
Future<void> write({required String rawState}) async {
_record = SyncStateRecord(rawState: rawState);
}
}
@@ -0,0 +1,14 @@
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_shared_prefs.dart';
/// Chooses sync queue persistence backend.
final Provider<SyncStateStore> syncStateStoreProvider =
Provider<SyncStateStore>((Ref ref) {
if (AppConfig.useHiveLocalDb) {
return HiveSyncStateStore();
}
return SharedPrefsSyncStateStore();
});
@@ -0,0 +1,32 @@
import 'package:relationship_saver/features/sync/storage/sync_state_store.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// shared_preferences-backed sync state store.
class SharedPrefsSyncStateStore implements SyncStateStore {
SharedPrefsSyncStateStore({this.stateKey = 'sync_queue_state_v1'});
final String stateKey;
@override
Future<void> clear() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.remove(stateKey);
}
@override
Future<SyncStateRecord?> read() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
final String? rawState = prefs.getString(stateKey);
if (rawState == null || rawState.isEmpty) {
return null;
}
return SyncStateRecord(rawState: rawState);
}
@override
Future<void> write({required String rawState}) async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString(stateKey, rawState);
}
}
+27 -10
View File
@@ -1,27 +1,43 @@
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';
import 'package:shared_preferences/shared_preferences.dart';
/// Persists queued local mutations and sync cursor metadata.
class SyncQueueRepository extends AsyncNotifier<SyncState> {
static const String _storageKey = 'sync_queue_state_v1';
@override
Future<SyncState> build() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
final String? raw = prefs.getString(_storageKey);
if (raw == null || raw.isEmpty) {
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(raw) as Map<String, dynamic>;
final Map<String, dynamic> json =
jsonDecode(record.rawState) as Map<String, dynamic>;
return SyncState.fromJson(json);
} on FormatException {
await prefs.remove(_storageKey);
await store.clear();
return SyncState.empty;
}
}
@@ -121,8 +137,9 @@ class SyncQueueRepository extends AsyncNotifier<SyncState> {
Future<void> _setState(SyncState next) async {
state = AsyncData<SyncState>(next);
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString(_storageKey, jsonEncode(next.toJson()));
await ref
.read(syncStateStoreProvider)
.write(rawState: jsonEncode(next.toJson()));
}
}