From d4ffddafb0c9559430f27e9718f2e957fe1b0059 Mon Sep 17 00:00:00 2001 From: bponders12-cell Date: Tue, 1 Sep 2026 16:56:49 -0400 Subject: [PATCH] Restore SyncManagerState copy semantics --- lib/services/sync/sync_models.dart | 163 +++++++---------------------- 1 file changed, 40 insertions(+), 123 deletions(-) diff --git a/lib/services/sync/sync_models.dart b/lib/services/sync/sync_models.dart index f988bcd..9e86c99 100644 --- a/lib/services/sync/sync_models.dart +++ b/lib/services/sync/sync_models.dart @@ -1,6 +1,5 @@ import 'package:flutter/foundation.dart'; -/// Persisted synchronization lifecycle. enum SyncStatus { localOnly, queued, @@ -17,11 +16,9 @@ enum SyncStatus { bool get isInProgress => this == uploading; } -/// Immutable outbox item. Raw telemetry is never stored here. @immutable class SyncItem { static const int schemaVersion = 2; - final int localRideId; final String providerId; final SyncStatus status; @@ -34,65 +31,30 @@ class SyncItem { 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, - }); + 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}); - /// Stable logical identity for one ride/provider destination. String get key => '$providerId:$localRideId'; - - /// Stable idempotency identity shared by retries and app restarts. 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!); - } + 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, - }) { - return 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, - ); - } + 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 toJson() => { 'schemaVersion': schemaVersion, @@ -110,62 +72,27 @@ class SyncItem { }; factory SyncItem.fromJson(Map json) { - final localRideId = (json['localRideId'] as num?)?.toInt(); - final providerId = json['providerId']; - final createdAtRaw = json['createdAt']; - if (localRideId == null || localRideId <= 0 || providerId is! String || providerId.trim().isEmpty || createdAtRaw is! String) { - throw const FormatException('Invalid sync item identity or createdAt'); + 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 statusName = json['status'] as String?; - final status = SyncStatus.values.firstWhere( - (e) => e.name == statusName, - orElse: () => SyncStatus.queued, - ); - final createdAt = DateTime.tryParse(createdAtRaw); - if (createdAt == null) throw const FormatException('Invalid sync item createdAt'); - - DateTime? parseDate(String key) { - final value = json[key]; - if (value == null) return null; - if (value is! String) throw FormatException('Invalid $key'); - return DateTime.tryParse(value); - } - final attempts = (json['attemptCount'] as num?)?.toInt() ?? 0; if (attempts < 0) throw const FormatException('Negative attemptCount'); - - return SyncItem( - localRideId: localRideId, - providerId: providerId.trim(), - status: status, - createdAt: createdAt, - attemptCount: attempts, - lastAttemptAt: parseDate('lastAttemptAt'), - nextRetryAt: parseDate('nextRetryAt'), - uploadedAt: parseDate('uploadedAt'), - remoteId: json['remoteId'] as String?, - lastError: json['lastError'] as String?, - bundleLocalPath: json['bundleLocalPath'] as String?, - ); + 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; - + 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); - - @override - String toString() => 'SyncItem(ride: $localRideId, provider: $providerId, status: ${status.name}, attempts: $attemptCount, nextRetry: $nextRetryAt)'; } @immutable @@ -176,23 +103,11 @@ class SyncResult { 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({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); - - @override - String toString() => 'SyncResult(success: $success, remoteId: $remoteId, error: $errorMessage, retryable: $isRetryable, auth: $requiresAuth)'; } @immutable @@ -205,13 +120,15 @@ class SyncManagerState { 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', - }); + 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, + ); }