293 lines
8.7 KiB
Dart
293 lines
8.7 KiB
Dart
import 'package:logging/logging.dart';
|
|
|
|
import '../models/change.dart';
|
|
import '../models/sheet.dart';
|
|
import 'api_client.dart';
|
|
import 'storage_service.dart';
|
|
|
|
/// Result of a sync operation.
|
|
class SyncResult {
|
|
final List<Sheet> sheets;
|
|
final bool isOnline;
|
|
final int changesSynced;
|
|
final int annotationsSynced;
|
|
final int changesUnsynced;
|
|
final int annotationsUnsynced;
|
|
|
|
SyncResult({
|
|
required this.sheets,
|
|
required this.isOnline,
|
|
required this.changesSynced,
|
|
required this.annotationsSynced,
|
|
required this.changesUnsynced,
|
|
required this.annotationsUnsynced,
|
|
});
|
|
}
|
|
|
|
/// Service for coordinating offline/online synchronization.
|
|
///
|
|
/// Handles:
|
|
/// - Fetching sheets with offline fallback to cached data
|
|
/// - Uploading pending changes when connection is available
|
|
/// - Uploading pending annotation uploads
|
|
/// - Applying local changes to sheets list
|
|
class SyncService {
|
|
final _log = Logger('SyncService');
|
|
final ApiClient _apiClient;
|
|
final StorageService _storageService;
|
|
|
|
SyncService({
|
|
required ApiClient apiClient,
|
|
required StorageService storageService,
|
|
}) : _apiClient = apiClient,
|
|
_storageService = storageService;
|
|
|
|
/// Performs a full sync operation.
|
|
///
|
|
/// 1. Checks if online
|
|
/// 2. If online: fetches sheets, uploads pending changes, uploads pending annotations
|
|
/// 3. If offline: loads cached sheets and applies pending changes locally
|
|
///
|
|
/// Returns [SyncResult] with the sheets list and sync status.
|
|
Future<SyncResult> sync() async {
|
|
final isOnline = await _apiClient.checkConnection();
|
|
|
|
if (isOnline) {
|
|
return _syncOnline();
|
|
} else {
|
|
return _syncOffline();
|
|
}
|
|
}
|
|
|
|
/// Online sync: fetch from server, upload pending data.
|
|
Future<SyncResult> _syncOnline() async {
|
|
_log.info('Online sync starting...');
|
|
|
|
int changesSynced = 0;
|
|
int annotationsSynced = 0;
|
|
|
|
// 1. Fetch fresh sheets from server
|
|
List<Sheet> sheets;
|
|
try {
|
|
sheets = await _apiClient.fetchSheets();
|
|
_log.info('Fetched ${sheets.length} sheets from server');
|
|
|
|
// Cache the fetched sheets
|
|
await _storageService.writeCachedSheets(sheets);
|
|
} catch (e) {
|
|
_log.warning('Failed to fetch sheets, falling back to cache: $e');
|
|
return _syncOffline();
|
|
}
|
|
|
|
// 2. Upload pending changes
|
|
changesSynced = await _uploadPendingChanges();
|
|
|
|
// 3. Upload pending annotations
|
|
annotationsSynced = await _uploadPendingAnnotations();
|
|
final remainingAnnotations = await _storageService
|
|
.readPendingAnnotationUploads();
|
|
|
|
// 4. Apply any remaining local changes (in case some failed to upload)
|
|
final changeQueue = await _storageService.readChangeQueue();
|
|
if (changeQueue.isNotEmpty) {
|
|
try {
|
|
changeQueue.applyToSheets(sheets);
|
|
// Update cache with applied changes
|
|
await _storageService.writeCachedSheets(sheets);
|
|
} catch (e) {
|
|
_log.warning('Failed to apply remaining changes: $e');
|
|
}
|
|
}
|
|
|
|
_log.info(
|
|
'Online sync complete: $changesSynced changes, $annotationsSynced annotations synced',
|
|
);
|
|
|
|
return SyncResult(
|
|
sheets: sheets,
|
|
isOnline: true,
|
|
changesSynced: changesSynced,
|
|
annotationsSynced: annotationsSynced,
|
|
changesUnsynced: changeQueue.length,
|
|
annotationsUnsynced: remainingAnnotations.length,
|
|
);
|
|
}
|
|
|
|
/// Offline sync: use cached data with local changes applied.
|
|
Future<SyncResult> _syncOffline() async {
|
|
_log.info('Offline mode: loading cached data...');
|
|
|
|
// 1. Load cached sheets
|
|
var sheets = await _storageService.readCachedSheets();
|
|
|
|
if (sheets.isEmpty) {
|
|
_log.warning('No cached sheets available in offline mode');
|
|
}
|
|
|
|
// 2. Apply pending changes locally
|
|
final changeQueue = await _storageService.readChangeQueue();
|
|
if (changeQueue.isNotEmpty) {
|
|
_log.info('Applying ${changeQueue.length} pending changes locally');
|
|
try {
|
|
changeQueue.applyToSheets(sheets);
|
|
} catch (e) {
|
|
_log.warning('Failed to apply some changes: $e');
|
|
}
|
|
}
|
|
|
|
final remainingAnnotations = await _storageService
|
|
.readPendingAnnotationUploads();
|
|
|
|
return SyncResult(
|
|
sheets: sheets,
|
|
isOnline: false,
|
|
changesSynced: 0,
|
|
annotationsSynced: 0,
|
|
changesUnsynced: changeQueue.length,
|
|
annotationsUnsynced: remainingAnnotations.length,
|
|
);
|
|
}
|
|
|
|
/// Uploads all pending changes to the server.
|
|
///
|
|
/// Returns the number of successfully synced changes.
|
|
Future<int> _uploadPendingChanges() async {
|
|
final changes = await _storageService.readChangeList();
|
|
if (changes.isEmpty) return 0;
|
|
|
|
_log.info('Uploading ${changes.length} pending changes...');
|
|
|
|
try {
|
|
final appliedIndices = await _apiClient.uploadChanges(changes);
|
|
|
|
// Delete successfully synced changes (in reverse order to maintain indices)
|
|
for (int i = appliedIndices.length - 1; i >= 0; i--) {
|
|
await _storageService.deleteOldestChange();
|
|
}
|
|
|
|
_log.info('${appliedIndices.length} changes synced successfully');
|
|
return appliedIndices.length;
|
|
} catch (e) {
|
|
_log.warning('Failed to upload changes: $e');
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/// Uploads all pending annotation uploads to the server.
|
|
///
|
|
/// Returns the number of successfully synced annotations.
|
|
Future<int> _uploadPendingAnnotations() async {
|
|
final pendingUploads = await _storageService.readPendingAnnotationUploads();
|
|
if (pendingUploads.isEmpty) return 0;
|
|
|
|
_log.info('Uploading ${pendingUploads.length} pending annotations...');
|
|
|
|
int syncedCount = 0;
|
|
|
|
for (final upload in pendingUploads) {
|
|
try {
|
|
await _apiClient.uploadAnnotation(
|
|
sheetUuid: upload.sheetUuid,
|
|
page: upload.page,
|
|
lastModified: upload.lastModified,
|
|
annotationsJson: upload.annotationsJson,
|
|
);
|
|
|
|
// Delete from pending queue after successful upload
|
|
await _storageService.deletePendingAnnotationUpload(upload.key);
|
|
syncedCount++;
|
|
} catch (e) {
|
|
_log.warning(
|
|
'Failed to upload annotation for ${upload.sheetUuid} page ${upload.page}: $e',
|
|
);
|
|
// Continue with other uploads
|
|
}
|
|
}
|
|
|
|
_log.info('$syncedCount annotations synced successfully');
|
|
return syncedCount;
|
|
}
|
|
|
|
/// Queues a change for sync.
|
|
///
|
|
/// If online, attempts immediate upload. Otherwise, stores locally.
|
|
Future<void> queueChange(Change change) async {
|
|
// Always store locally first
|
|
await _storageService.writeChange(change);
|
|
|
|
// Try to upload immediately if online
|
|
try {
|
|
final isOnline = await _apiClient.checkConnection();
|
|
if (isOnline) {
|
|
final changes = await _storageService.readChangeList();
|
|
final appliedIndices = await _apiClient.uploadChanges(changes);
|
|
|
|
// Delete synced changes
|
|
for (int i = 0; i < appliedIndices.length; i++) {
|
|
await _storageService.deleteOldestChange();
|
|
}
|
|
}
|
|
} catch (e) {
|
|
_log.fine('Immediate upload failed, change queued for later: $e');
|
|
}
|
|
}
|
|
|
|
/// Queues an annotation upload.
|
|
///
|
|
/// If the upload fails (e.g., offline), it will be stored for later sync.
|
|
Future<bool> uploadAnnotationWithFallback({
|
|
required String sheetUuid,
|
|
required int page,
|
|
required String annotationsJson,
|
|
required DateTime lastModified,
|
|
}) async {
|
|
try {
|
|
await _apiClient.uploadAnnotation(
|
|
sheetUuid: sheetUuid,
|
|
page: page,
|
|
lastModified: lastModified,
|
|
annotationsJson: annotationsJson,
|
|
);
|
|
return true;
|
|
} catch (e) {
|
|
_log.fine('Annotation upload failed, queuing for later: $e');
|
|
|
|
// Store for later upload
|
|
await _storageService.writePendingAnnotationUpload(
|
|
PendingAnnotationUpload(
|
|
sheetUuid: sheetUuid,
|
|
page: page,
|
|
annotationsJson: annotationsJson,
|
|
lastModified: lastModified,
|
|
),
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Updates the local cache after a sheet edit.
|
|
///
|
|
/// Call this after applying changes to the sheets list locally.
|
|
Future<void> updateCachedSheets(List<Sheet> sheets) async {
|
|
await _storageService.writeCachedSheets(sheets);
|
|
}
|
|
|
|
/// Gets the number of pending changes.
|
|
Future<int> getPendingChangesCount() async {
|
|
return _storageService.getChangeQueueLength();
|
|
}
|
|
|
|
/// Gets the number of pending annotation uploads.
|
|
Future<int> getPendingAnnotationsCount() async {
|
|
final uploads = await _storageService.readPendingAnnotationUploads();
|
|
return uploads.length;
|
|
}
|
|
|
|
/// Checks if there is any pending data to sync.
|
|
Future<bool> hasPendingData() async {
|
|
final changesCount = await getPendingChangesCount();
|
|
final annotationsCount = await getPendingAnnotationsCount();
|
|
return changesCount > 0 || annotationsCount > 0;
|
|
}
|
|
}
|