aves_mio22/lib/remote/remote_sync_engine.dart
2026-04-18 20:05:02 +02:00

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();
}
}