360 lines
9.7 KiB
Dart
360 lines
9.7 KiB
Dart
import 'dart:async';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||
|
||
import 'package:aves/model/source/collection_source.dart';
|
||
import 'package:aves/remote/remote_settings.dart';
|
||
import 'package:aves/remote/remote_sync_bus.dart';
|
||
import 'package:aves/remote/remote_repository.dart';
|
||
import 'package:aves/services/common/services.dart';
|
||
import 'package:aves/remote/remote_client.dart';
|
||
import 'package:aves/remote/auth_client.dart';
|
||
import 'package:aves/remote/collection_source_remote_ext.dart';
|
||
|
||
import 'remote_origin.dart';
|
||
|
||
class RemoteController {
|
||
RemoteController._();
|
||
static final RemoteController instance = RemoteController._();
|
||
|
||
static const _kBootstrapDone = 'remote_bootstrap_done';
|
||
|
||
bool _syncInFlight = false;
|
||
|
||
// 🔥 Retry basato sul tempo reale
|
||
DateTime? _retryStartTime;
|
||
Timer? _retryTimer;
|
||
|
||
Future<bool> bootstrapDone() async {
|
||
final storage = FlutterSecureStorage();
|
||
return (await storage.read(key: _kBootstrapDone)) == '1';
|
||
}
|
||
|
||
Future<void> _setBootstrapDone() async {
|
||
final storage = FlutterSecureStorage();
|
||
await storage.write(key: _kBootstrapDone, value: '1');
|
||
}
|
||
|
||
// 🔥 Stato iniziale corretto: arancione lampeggiante
|
||
Future<void> initBusFromSettings() async {
|
||
final s = await RemoteSettings.load();
|
||
|
||
if (!s.enabled) {
|
||
RemoteSyncBus.instance.setDisabled();
|
||
return;
|
||
}
|
||
|
||
RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.syncing;
|
||
}
|
||
|
||
Future<void> onAppStart({
|
||
required CollectionSource source,
|
||
bool resumeBootstrapIfEnabled = true,
|
||
}) async {
|
||
final s = await RemoteSettings.load();
|
||
|
||
if (!s.enabled) {
|
||
RemoteSyncBus.instance.setDisabled();
|
||
final remotesInMemory =
|
||
source.allEntries.where((e) => e.origin == RemoteOrigin.value).toSet();
|
||
if (remotesInMemory.isNotEmpty) {
|
||
source.removeEntriesFromMemory(remotesInMemory);
|
||
}
|
||
return;
|
||
}
|
||
|
||
final done = await bootstrapDone();
|
||
if (done) {
|
||
await source.appendRemoteEntriesFromDb();
|
||
unawaited(fullSync(source: source, showOverlay: false));
|
||
} else {
|
||
if (resumeBootstrapIfEnabled) {
|
||
unawaited(fullSync(
|
||
source: source,
|
||
showOverlay: true,
|
||
markBootstrapDoneOnSuccess: true,
|
||
));
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<void> toggleRemote({required CollectionSource source}) async {
|
||
final s = await RemoteSettings.load();
|
||
|
||
if (s.enabled) {
|
||
final upd = RemoteSettings(
|
||
enabled: false,
|
||
baseUrl: s.baseUrl,
|
||
indexPath: s.indexPath,
|
||
email: s.email,
|
||
password: s.password,
|
||
);
|
||
await upd.save();
|
||
|
||
debugPrint('[remote] toggle -> OFF');
|
||
|
||
final remotesInMemory =
|
||
source.allEntries.where((e) => e.origin == RemoteOrigin.value).toSet();
|
||
if (remotesInMemory.isNotEmpty) {
|
||
source.removeEntriesFromMemory(remotesInMemory);
|
||
}
|
||
|
||
RemoteSyncBus.instance.setDisabled();
|
||
|
||
_retryTimer?.cancel();
|
||
_retryTimer = null;
|
||
_retryStartTime = null;
|
||
|
||
return;
|
||
}
|
||
|
||
final upd = RemoteSettings(
|
||
enabled: true,
|
||
baseUrl: s.baseUrl,
|
||
indexPath: s.indexPath,
|
||
email: s.email,
|
||
password: s.password,
|
||
);
|
||
await upd.save();
|
||
|
||
debugPrint('[remote] toggle -> ON');
|
||
|
||
await source.appendRemoteEntriesFromDb();
|
||
unawaited(fullSync(source: source, showOverlay: false));
|
||
}
|
||
|
||
// 🔥 Retry basato sul tempo reale
|
||
void _scheduleRetry(CollectionSource source) {
|
||
_retryTimer?.cancel();
|
||
|
||
_retryTimer = Timer(const Duration(seconds: 30), () async {
|
||
final s = await RemoteSettings.load();
|
||
if (!s.enabled) return;
|
||
|
||
// 🔥 Controllo tempo reale passato
|
||
if (_retryStartTime != null) {
|
||
final elapsed = DateTime.now().difference(_retryStartTime!);
|
||
if (elapsed > const Duration(minutes: 5)) {
|
||
print('[remote] retry timeout → disattivo remote');
|
||
|
||
final remotesInMemory =
|
||
source.allEntries.where((e) => e.origin == RemoteOrigin.value).toSet();
|
||
if (remotesInMemory.isNotEmpty) {
|
||
source.removeEntriesFromMemory(remotesInMemory);
|
||
}
|
||
|
||
final upd = RemoteSettings(
|
||
enabled: false,
|
||
baseUrl: s.baseUrl,
|
||
indexPath: s.indexPath,
|
||
email: s.email,
|
||
password: s.password,
|
||
);
|
||
await upd.save();
|
||
|
||
RemoteSyncBus.instance.setDisabled();
|
||
return;
|
||
}
|
||
}
|
||
|
||
print('[remote] retry ping…');
|
||
|
||
final auth = (s.email.isNotEmpty && s.password.isNotEmpty)
|
||
? RemoteAuth(
|
||
baseUrl: s.baseUrl,
|
||
email: s.email,
|
||
password: s.password,
|
||
)
|
||
: null;
|
||
|
||
final retryClient = RemoteJsonClient(
|
||
s.baseUrl,
|
||
s.indexPath,
|
||
auth: auth,
|
||
);
|
||
|
||
try {
|
||
await retryClient.ping().timeout(const Duration(seconds: 3));
|
||
|
||
print('[remote] retry OK → riprendo sync');
|
||
|
||
_retryTimer = null;
|
||
_retryStartTime = null;
|
||
|
||
unawaited(fullSync(source: source, showOverlay: false));
|
||
return;
|
||
|
||
} catch (_) {
|
||
print('[remote] retry fallito');
|
||
_scheduleRetry(source);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 🔥 fullSync pulito, senza retry interno
|
||
Future<void> fullSync({
|
||
required CollectionSource source,
|
||
required bool showOverlay,
|
||
bool markBootstrapDoneOnSuccess = false,
|
||
}) async {
|
||
if (_syncInFlight) {
|
||
debugPrint('[remote] sync skipped (already in flight)');
|
||
return;
|
||
}
|
||
_syncInFlight = true;
|
||
|
||
final s = await RemoteSettings.load();
|
||
if (!s.enabled) {
|
||
RemoteSyncBus.instance.setDisabled();
|
||
_syncInFlight = false;
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (s.baseUrl.trim().isEmpty) {
|
||
RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown;
|
||
|
||
_retryStartTime ??= DateTime.now();
|
||
_scheduleRetry(source);
|
||
|
||
return;
|
||
}
|
||
|
||
RemoteAuth? auth;
|
||
if (s.email.isNotEmpty && s.password.isNotEmpty) {
|
||
auth = RemoteAuth(
|
||
baseUrl: s.baseUrl,
|
||
email: s.email,
|
||
password: s.password,
|
||
);
|
||
}
|
||
|
||
final client = RemoteJsonClient(s.baseUrl, s.indexPath, auth: auth);
|
||
|
||
// 1️⃣ PING
|
||
await client.ping().timeout(const Duration(seconds: 3));
|
||
|
||
// 2️⃣ FETCH LISTA
|
||
final items = await client.fetchAll().timeout(const Duration(seconds: 30));
|
||
final total = items.length;
|
||
|
||
// 3️⃣ BANNER
|
||
final opId =
|
||
RemoteSyncBus.instance.start(total: total, showOverlay: showOverlay);
|
||
|
||
final repo = RemoteRepository(localMediaDb.rawDb);
|
||
await repo.deleteAllRemotes();
|
||
|
||
const chunkSize = 200;
|
||
int done = 0;
|
||
final serverIds =
|
||
items.map((e) => e.id).where((v) => v.isNotEmpty).toSet();
|
||
|
||
for (var offset = 0; offset < total; offset += chunkSize) {
|
||
final end =
|
||
(offset + chunkSize < total) ? offset + chunkSize : total;
|
||
await repo.upsertAll(items.sublist(offset, end),
|
||
chunkSize: chunkSize);
|
||
done = end;
|
||
RemoteSyncBus.instance.update(
|
||
opId: opId, done: done, total: total);
|
||
}
|
||
|
||
await repo.pruneMissingRemotes(serverIds);
|
||
await source.appendRemoteEntriesFromDb();
|
||
|
||
if (markBootstrapDoneOnSuccess) {
|
||
await _setBootstrapDone();
|
||
}
|
||
|
||
RemoteSyncBus.instance.finishUpToDate(opId: opId);
|
||
|
||
// 🔥 Sync OK → stop retry
|
||
_retryTimer?.cancel();
|
||
_retryTimer = null;
|
||
_retryStartTime = null;
|
||
|
||
} catch (e) {
|
||
print('[remote] error during sync: $e');
|
||
|
||
final remotesInMemory =
|
||
source.allEntries.where((e) => e.origin == RemoteOrigin.value).toSet();
|
||
if (remotesInMemory.isNotEmpty) {
|
||
source.removeEntriesFromMemory(remotesInMemory);
|
||
}
|
||
|
||
RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown;
|
||
|
||
// 🔥 Avvia retry esterno basato sul tempo reale
|
||
_retryStartTime ??= DateTime.now();
|
||
_scheduleRetry(source);
|
||
|
||
} finally {
|
||
_syncInFlight = false;
|
||
}
|
||
}
|
||
|
||
// 🔥 Ping immediato al resume dell’app
|
||
Future<void> onResume(CollectionSource source) async {
|
||
final s = await RemoteSettings.load();
|
||
if (!s.enabled) return;
|
||
|
||
// Se siamo in retry, controlla timeout
|
||
if (_retryStartTime != null) {
|
||
final elapsed = DateTime.now().difference(_retryStartTime!);
|
||
if (elapsed > const Duration(minutes: 5)) {
|
||
print('[remote] resume → timeout superato → disattivo remote');
|
||
|
||
final remotesInMemory =
|
||
source.allEntries.where((e) => e.origin == RemoteOrigin.value).toSet();
|
||
if (remotesInMemory.isNotEmpty) {
|
||
source.removeEntriesFromMemory(remotesInMemory);
|
||
}
|
||
|
||
final upd = RemoteSettings(
|
||
enabled: false,
|
||
baseUrl: s.baseUrl,
|
||
indexPath: s.indexPath,
|
||
email: s.email,
|
||
password: s.password,
|
||
);
|
||
await upd.save();
|
||
|
||
RemoteSyncBus.instance.setDisabled();
|
||
return;
|
||
}
|
||
}
|
||
|
||
print('[remote] resume → ping immediato');
|
||
|
||
final auth = (s.email.isNotEmpty && s.password.isNotEmpty)
|
||
? RemoteAuth(
|
||
baseUrl: s.baseUrl,
|
||
email: s.email,
|
||
password: s.password,
|
||
)
|
||
: null;
|
||
|
||
final client = RemoteJsonClient(
|
||
s.baseUrl,
|
||
s.indexPath,
|
||
auth: auth,
|
||
);
|
||
|
||
try {
|
||
await client.ping().timeout(const Duration(seconds: 3));
|
||
|
||
print('[remote] resume → ping OK → sync immediata');
|
||
|
||
_retryTimer?.cancel();
|
||
_retryTimer = null;
|
||
_retryStartTime = null;
|
||
|
||
unawaited(fullSync(source: source, showOverlay: false));
|
||
} catch (_) {
|
||
print('[remote] resume → ping fallito → scheduleRetry');
|
||
_retryStartTime ??= DateTime.now();
|
||
_scheduleRetry(source);
|
||
}
|
||
}
|
||
}
|