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

360 lines
9.7 KiB
Text
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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