MotorSignal-App/lib/services/sync/sync_models.dart

135 lines
6.8 KiB
Dart

import 'package:flutter/foundation.dart';
enum SyncStatus {
localOnly,
queued,
waitingForNetwork,
uploading,
synced,
failed,
retryWait,
authRequired,
cancelled;
bool get isTerminal => this == synced || this == failed || this == cancelled;
bool get isPending => this == queued || this == waitingForNetwork || this == retryWait || this == authRequired;
bool get isInProgress => this == uploading;
}
@immutable
class SyncItem {
static const int schemaVersion = 2;
final int localRideId;
final String providerId;
final SyncStatus status;
final DateTime createdAt;
final int attemptCount;
final DateTime? lastAttemptAt;
final DateTime? nextRetryAt;
final DateTime? uploadedAt;
final String? remoteId;
final String? lastError;
final String? bundleLocalPath;
const SyncItem({required this.localRideId, required this.providerId, this.status = SyncStatus.queued, required this.createdAt, this.attemptCount = 0, this.lastAttemptAt, this.nextRetryAt, this.uploadedAt, this.remoteId, this.lastError, this.bundleLocalPath});
String get key => '$providerId:$localRideId';
String get idempotencyKey => key;
bool isEligibleForAttempt(DateTime now) {
if (status.isTerminal || status == SyncStatus.uploading) return false;
if (status == SyncStatus.retryWait && nextRetryAt != null) return !now.isBefore(nextRetryAt!);
return true;
}
SyncItem copyWith({int? localRideId, String? providerId, SyncStatus? status, DateTime? createdAt, int? attemptCount, DateTime? lastAttemptAt, DateTime? nextRetryAt, DateTime? uploadedAt, String? remoteId, String? lastError, String? bundleLocalPath, bool clearLastError = false, bool clearNextRetry = false, bool clearRemoteId = false, bool clearUploadedAt = false}) => SyncItem(
localRideId: localRideId ?? this.localRideId,
providerId: providerId ?? this.providerId,
status: status ?? this.status,
createdAt: createdAt ?? this.createdAt,
attemptCount: attemptCount ?? this.attemptCount,
lastAttemptAt: lastAttemptAt ?? this.lastAttemptAt,
nextRetryAt: clearNextRetry ? null : (nextRetryAt ?? this.nextRetryAt),
uploadedAt: clearUploadedAt ? null : (uploadedAt ?? this.uploadedAt),
remoteId: clearRemoteId ? null : (remoteId ?? this.remoteId),
lastError: clearLastError ? null : (lastError ?? this.lastError),
bundleLocalPath: bundleLocalPath ?? this.bundleLocalPath,
);
Map<String, dynamic> toJson() => {
'schemaVersion': schemaVersion,
'localRideId': localRideId,
'providerId': providerId,
'status': status.name,
'createdAt': createdAt.toIso8601String(),
'attemptCount': attemptCount,
'lastAttemptAt': lastAttemptAt?.toIso8601String(),
'nextRetryAt': nextRetryAt?.toIso8601String(),
'uploadedAt': uploadedAt?.toIso8601String(),
'remoteId': remoteId,
'lastError': lastError,
'bundleLocalPath': bundleLocalPath,
};
factory SyncItem.fromJson(Map<String, dynamic> json) {
final id = (json['localRideId'] as num?)?.toInt();
final provider = json['providerId'];
final createdRaw = json['createdAt'];
if (id == null || id <= 0 || provider is! String || provider.trim().isEmpty || createdRaw is! String) throw const FormatException('Invalid sync item identity');
final created = DateTime.tryParse(createdRaw);
if (created == null) throw const FormatException('Invalid createdAt');
DateTime? date(String key) {
final v = json[key];
if (v == null) return null;
if (v is! String) throw FormatException('Invalid $key');
return DateTime.tryParse(v);
}
final attempts = (json['attemptCount'] as num?)?.toInt() ?? 0;
if (attempts < 0) throw const FormatException('Negative attemptCount');
return SyncItem(localRideId: id, providerId: provider.trim(), status: SyncStatus.values.firstWhere((e) => e.name == json['status'], orElse: () => SyncStatus.queued), createdAt: created, attemptCount: attempts, lastAttemptAt: date('lastAttemptAt'), nextRetryAt: date('nextRetryAt'), uploadedAt: date('uploadedAt'), remoteId: json['remoteId'] as String?, lastError: json['lastError'] as String?, bundleLocalPath: json['bundleLocalPath'] as String?);
}
@override
bool operator ==(Object other) => identical(this, other) || other is SyncItem && localRideId == other.localRideId && providerId == other.providerId && status == other.status && attemptCount == other.attemptCount && nextRetryAt == other.nextRetryAt && remoteId == other.remoteId && lastError == other.lastError;
@override
int get hashCode => Object.hash(localRideId, providerId, status, attemptCount, nextRetryAt, remoteId, lastError);
}
@immutable
class SyncResult {
final bool success;
final String? remoteId;
final String? errorMessage;
final bool isRetryable;
final bool requiresAuth;
final DateTime timestamp;
SyncResult({required this.success, this.remoteId, this.errorMessage, this.isRetryable = true, this.requiresAuth = false, DateTime? timestamp}) : timestamp = timestamp ?? DateTime.now();
SyncResult.success({required String remoteId, DateTime? timestamp}) : this(success: true, remoteId: remoteId, isRetryable: false, requiresAuth: false, timestamp: timestamp);
SyncResult.temporaryFailure(String message, {DateTime? timestamp}) : this(success: false, errorMessage: message, isRetryable: true, timestamp: timestamp);
SyncResult.permanentFailure(String message, {DateTime? timestamp}) : this(success: false, errorMessage: message, isRetryable: false, timestamp: timestamp);
SyncResult.authRequired({String message = 'Authentication credentials required for sync', DateTime? timestamp}) : this(success: false, errorMessage: message, isRetryable: true, requiresAuth: true, timestamp: timestamp);
}
@immutable
class SyncManagerState {
final bool isProcessing;
final int pendingCount;
final int syncedCount;
final int failedCount;
final DateTime? lastSuccessfulSync;
final String? lastError;
final String activeProviderId;
const SyncManagerState({this.isProcessing = false, this.pendingCount = 0, this.syncedCount = 0, this.failedCount = 0, this.lastSuccessfulSync, this.lastError, this.activeProviderId = 'none'});
SyncManagerState copyWith({bool? isProcessing, int? pendingCount, int? syncedCount, int? failedCount, DateTime? lastSuccessfulSync, String? lastError, String? activeProviderId, bool clearLastError = false}) => SyncManagerState(
isProcessing: isProcessing ?? this.isProcessing,
pendingCount: pendingCount ?? this.pendingCount,
syncedCount: syncedCount ?? this.syncedCount,
failedCount: failedCount ?? this.failedCount,
lastSuccessfulSync: lastSuccessfulSync ?? this.lastSuccessfulSync,
lastError: clearLastError ? null : (lastError ?? this.lastError),
activeProviderId: activeProviderId ?? this.activeProviderId,
);
}