Restore SyncManagerState copy semantics
This commit is contained in:
parent
f6d5d34368
commit
d4ffddafb0
|
|
@ -1,6 +1,5 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
/// Persisted synchronization lifecycle.
|
|
||||||
enum SyncStatus {
|
enum SyncStatus {
|
||||||
localOnly,
|
localOnly,
|
||||||
queued,
|
queued,
|
||||||
|
|
@ -17,11 +16,9 @@ enum SyncStatus {
|
||||||
bool get isInProgress => this == uploading;
|
bool get isInProgress => this == uploading;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Immutable outbox item. Raw telemetry is never stored here.
|
|
||||||
@immutable
|
@immutable
|
||||||
class SyncItem {
|
class SyncItem {
|
||||||
static const int schemaVersion = 2;
|
static const int schemaVersion = 2;
|
||||||
|
|
||||||
final int localRideId;
|
final int localRideId;
|
||||||
final String providerId;
|
final String providerId;
|
||||||
final SyncStatus status;
|
final SyncStatus status;
|
||||||
|
|
@ -34,52 +31,18 @@ class SyncItem {
|
||||||
final String? lastError;
|
final String? lastError;
|
||||||
final String? bundleLocalPath;
|
final String? bundleLocalPath;
|
||||||
|
|
||||||
const SyncItem({
|
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});
|
||||||
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';
|
String get key => '$providerId:$localRideId';
|
||||||
|
|
||||||
/// Stable idempotency identity shared by retries and app restarts.
|
|
||||||
String get idempotencyKey => key;
|
String get idempotencyKey => key;
|
||||||
|
|
||||||
bool isEligibleForAttempt(DateTime now) {
|
bool isEligibleForAttempt(DateTime now) {
|
||||||
if (status.isTerminal || status == SyncStatus.uploading) return false;
|
if (status.isTerminal || status == SyncStatus.uploading) return false;
|
||||||
if (status == SyncStatus.retryWait && nextRetryAt != null) {
|
if (status == SyncStatus.retryWait && nextRetryAt != null) return !now.isBefore(nextRetryAt!);
|
||||||
return !now.isBefore(nextRetryAt!);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
SyncItem copyWith({
|
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(
|
||||||
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,
|
localRideId: localRideId ?? this.localRideId,
|
||||||
providerId: providerId ?? this.providerId,
|
providerId: providerId ?? this.providerId,
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
|
|
@ -92,7 +55,6 @@ class SyncItem {
|
||||||
lastError: clearLastError ? null : (lastError ?? this.lastError),
|
lastError: clearLastError ? null : (lastError ?? this.lastError),
|
||||||
bundleLocalPath: bundleLocalPath ?? this.bundleLocalPath,
|
bundleLocalPath: bundleLocalPath ?? this.bundleLocalPath,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'schemaVersion': schemaVersion,
|
'schemaVersion': schemaVersion,
|
||||||
|
|
@ -110,62 +72,27 @@ class SyncItem {
|
||||||
};
|
};
|
||||||
|
|
||||||
factory SyncItem.fromJson(Map<String, dynamic> json) {
|
factory SyncItem.fromJson(Map<String, dynamic> json) {
|
||||||
final localRideId = (json['localRideId'] as num?)?.toInt();
|
final id = (json['localRideId'] as num?)?.toInt();
|
||||||
final providerId = json['providerId'];
|
final provider = json['providerId'];
|
||||||
final createdAtRaw = json['createdAt'];
|
final createdRaw = json['createdAt'];
|
||||||
if (localRideId == null || localRideId <= 0 || providerId is! String || providerId.trim().isEmpty || createdAtRaw is! String) {
|
if (id == null || id <= 0 || provider is! String || provider.trim().isEmpty || createdRaw is! String) throw const FormatException('Invalid sync item identity');
|
||||||
throw const FormatException('Invalid sync item identity or createdAt');
|
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;
|
final attempts = (json['attemptCount'] as num?)?.toInt() ?? 0;
|
||||||
if (attempts < 0) throw const FormatException('Negative attemptCount');
|
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?);
|
||||||
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?,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) ||
|
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;
|
||||||
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
|
@override
|
||||||
int get hashCode => Object.hash(localRideId, providerId, status, attemptCount, nextRetryAt, remoteId, lastError);
|
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
|
@immutable
|
||||||
|
|
@ -176,23 +103,11 @@ class SyncResult {
|
||||||
final bool isRetryable;
|
final bool isRetryable;
|
||||||
final bool requiresAuth;
|
final bool requiresAuth;
|
||||||
final DateTime timestamp;
|
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.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.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.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);
|
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
|
@immutable
|
||||||
|
|
@ -205,13 +120,15 @@ class SyncManagerState {
|
||||||
final String? lastError;
|
final String? lastError;
|
||||||
final String activeProviderId;
|
final String activeProviderId;
|
||||||
|
|
||||||
const SyncManagerState({
|
const SyncManagerState({this.isProcessing = false, this.pendingCount = 0, this.syncedCount = 0, this.failedCount = 0, this.lastSuccessfulSync, this.lastError, this.activeProviderId = 'none'});
|
||||||
this.isProcessing = false,
|
|
||||||
this.pendingCount = 0,
|
SyncManagerState copyWith({bool? isProcessing, int? pendingCount, int? syncedCount, int? failedCount, DateTime? lastSuccessfulSync, String? lastError, String? activeProviderId, bool clearLastError = false}) => SyncManagerState(
|
||||||
this.syncedCount = 0,
|
isProcessing: isProcessing ?? this.isProcessing,
|
||||||
this.failedCount = 0,
|
pendingCount: pendingCount ?? this.pendingCount,
|
||||||
this.lastSuccessfulSync,
|
syncedCount: syncedCount ?? this.syncedCount,
|
||||||
this.lastError,
|
failedCount: failedCount ?? this.failedCount,
|
||||||
this.activeProviderId = 'none',
|
lastSuccessfulSync: lastSuccessfulSync ?? this.lastSuccessfulSync,
|
||||||
});
|
lastError: clearLastError ? null : (lastError ?? this.lastError),
|
||||||
|
activeProviderId: activeProviderId ?? this.activeProviderId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue