208 lines
5.7 KiB
Dart
208 lines
5.7 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:aves/model/source/collection_source.dart';
|
|
|
|
import 'remote_http_api.dart';
|
|
import 'remote_models.dart';
|
|
import 'remote_repository.dart';
|
|
import 'remote_state_store.dart';
|
|
import 'package:aves/remote/collection_source_remote_ext.dart';
|
|
|
|
class RemoteSyncEngine {
|
|
RemoteSyncEngine({
|
|
required this.api,
|
|
required this.repo,
|
|
required this.source,
|
|
required this.state,
|
|
});
|
|
|
|
final RemoteHttpApi api;
|
|
final RemoteRepository repo;
|
|
final CollectionSource source;
|
|
final RemoteStateStore state;
|
|
|
|
static const int retentionDays = 30;
|
|
static const Duration overlap = Duration(seconds: 3);
|
|
|
|
bool _syncInFlight = false;
|
|
bool _pendingSync = false;
|
|
|
|
bool _isTooOld(String iso) {
|
|
final t = DateTime.tryParse(iso);
|
|
if (t == null) return true;
|
|
return DateTime.now().toUtc().difference(t.toUtc()).inDays > retentionDays;
|
|
}
|
|
|
|
String _applyOverlap(String iso) {
|
|
final t = DateTime.tryParse(iso);
|
|
if (t == null) return iso;
|
|
return t.toUtc().subtract(overlap).toIso8601String();
|
|
}
|
|
|
|
// ------------------------------------------------------------
|
|
// Public API
|
|
// ------------------------------------------------------------
|
|
|
|
Future<void> progressiveSync() async {
|
|
return _runExclusive(() async {
|
|
final last = await state.getLastSyncIso();
|
|
|
|
if (last == null || last.isEmpty || _isTooOld(last)) {
|
|
await _fullSyncImpl();
|
|
return;
|
|
}
|
|
|
|
await _deltaSyncImpl(_applyOverlap(last));
|
|
});
|
|
}
|
|
|
|
Future<void> progressiveSyncFrom(String sinceIso) async {
|
|
return _runExclusive(() async {
|
|
if (sinceIso.isEmpty || _isTooOld(sinceIso)) {
|
|
await _fullSyncImpl();
|
|
return;
|
|
}
|
|
|
|
await _deltaSyncImpl(_applyOverlap(sinceIso));
|
|
});
|
|
}
|
|
|
|
// ------------------------------------------------------------
|
|
// FULL SYNC (senza _runExclusive)
|
|
// ------------------------------------------------------------
|
|
|
|
Future<void> _fullSyncImpl() async {
|
|
debugPrint('[remote] fullSync start');
|
|
|
|
final list = await api.getAllPhotos();
|
|
final items = list.map(RemotePhotoItem.fromJson).toList();
|
|
|
|
await repo.deleteAllRemotes();
|
|
await repo.upsertAll(items, chunkSize: 200);
|
|
|
|
await source.appendRemoteEntriesFromDb();
|
|
|
|
final next = _maxIsoFromItems(items) ??
|
|
DateTime.now().toUtc().toIso8601String();
|
|
|
|
await state.setLastSyncIso(next);
|
|
|
|
debugPrint('[remote] fullSync done items=${items.length} lastSync=$next');
|
|
}
|
|
|
|
// ------------------------------------------------------------
|
|
// DELTA SYNC (senza _runExclusive)
|
|
// ------------------------------------------------------------
|
|
|
|
Future<void> _deltaSyncImpl(String sinceIso,
|
|
{bool appendAfter = true}) async {
|
|
debugPrint('[remote] deltaSyncFrom since=$sinceIso');
|
|
|
|
final List<Map<String, dynamic>> changed = await api.getChanges(sinceIso);
|
|
final List<Map<String, dynamic>> deleted =
|
|
await api.getDeletedHard(sinceIso);
|
|
|
|
var didChange = false;
|
|
|
|
List<RemotePhotoItem> changedItems = const [];
|
|
if (changed.isNotEmpty) {
|
|
changedItems = changed.map(RemotePhotoItem.fromJson).toList();
|
|
await repo.upsertAll(changedItems, chunkSize: 200);
|
|
debugPrint('[remote] delta upsert=${changedItems.length}');
|
|
didChange = true;
|
|
}
|
|
|
|
if (deleted.isNotEmpty) {
|
|
final ids = deleted
|
|
.map((e) => e['id']?.toString())
|
|
.whereType<String>()
|
|
.where((s) => s.isNotEmpty)
|
|
.toSet();
|
|
|
|
if (ids.isNotEmpty) {
|
|
await repo.deleteRemotesByRemoteIds(ids);
|
|
debugPrint('[remote] delta hardDeleted=${ids.length}');
|
|
didChange = true;
|
|
}
|
|
}
|
|
|
|
if (appendAfter && didChange) {
|
|
await source.appendRemoteEntriesFromDb();
|
|
}
|
|
|
|
final next = _computeNextSyncIso(
|
|
changedItems: changedItems,
|
|
hardDeletedRaw: deleted,
|
|
) ??
|
|
DateTime.now().toUtc().toIso8601String();
|
|
|
|
await state.setLastSyncIso(next);
|
|
debugPrint('[remote] delta lastSync -> $next');
|
|
}
|
|
|
|
// ------------------------------------------------------------
|
|
// Exclusive wrapper (FIXATO)
|
|
// ------------------------------------------------------------
|
|
|
|
Future<void> _runExclusive(Future<void> Function() fn) async {
|
|
if (_syncInFlight) {
|
|
_pendingSync = true;
|
|
debugPrint('[remote] sync already in flight -> pending=true');
|
|
return;
|
|
}
|
|
|
|
_syncInFlight = true;
|
|
try {
|
|
await fn();
|
|
} finally {
|
|
_syncInFlight = false;
|
|
|
|
if (_pendingSync) {
|
|
_pendingSync = false;
|
|
debugPrint('[remote] run pending sync');
|
|
|
|
// ❗ NON ricorsivo
|
|
Future.microtask(() => _runExclusive(fn));
|
|
}
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------
|
|
// Helpers
|
|
// ------------------------------------------------------------
|
|
|
|
String? _maxIsoFromItems(List<RemotePhotoItem> items) {
|
|
DateTime? maxDt;
|
|
for (final it in items) {
|
|
final dt = it.updatedAtUtc ?? it.createdAtUtc;
|
|
if (dt != null && (maxDt == null || dt.isAfter(maxDt))) {
|
|
maxDt = dt;
|
|
}
|
|
}
|
|
return maxDt?.toUtc().toIso8601String();
|
|
}
|
|
|
|
String? _computeNextSyncIso({
|
|
required List<RemotePhotoItem> changedItems,
|
|
required List<Map<String, dynamic>> hardDeletedRaw,
|
|
}) {
|
|
DateTime? maxDt;
|
|
|
|
void consider(DateTime? dt) {
|
|
if (dt == null) return;
|
|
if (maxDt == null || dt.isAfter(maxDt!)) maxDt = dt;
|
|
}
|
|
|
|
for (final it in changedItems) {
|
|
consider(it.updatedAtUtc ?? it.createdAtUtc);
|
|
}
|
|
|
|
for (final d in hardDeletedRaw) {
|
|
final s = d['deleted_at']?.toString();
|
|
if (s != null && s.isNotEmpty) {
|
|
consider(DateTime.tryParse(s)?.toUtc());
|
|
}
|
|
}
|
|
|
|
return maxDt?.toUtc().toIso8601String();
|
|
}
|
|
}
|