Add sync queue orchestration and local mutation envelopes

This commit is contained in:
Rijad Zuzo
2026-02-15 16:56:55 +01:00
parent aaabc51d2d
commit d711f270dd
8 changed files with 1411 additions and 101 deletions
+98
View File
@@ -0,0 +1,98 @@
import 'package:flutter/foundation.dart';
import 'package:relationship_saver/integrations/backend/models/backend_models.dart';
/// Persisted state for sync queue and cursor progression.
@immutable
class SyncState {
const SyncState({
this.cursor,
required this.pendingChanges,
this.lastSyncAt,
this.lastAttemptAt,
this.lastError,
this.lastRejected = const <MutationRejection>[],
});
factory SyncState.fromJson(Map<String, dynamic> json) {
return SyncState(
cursor: json['cursor'] as String?,
pendingChanges: (json['pendingChanges'] as List<dynamic>? ?? <dynamic>[])
.map(
(dynamic item) =>
ChangeEnvelope.fromJson(item as Map<String, dynamic>),
)
.toList(growable: false),
lastSyncAt: _tryParseDate(json['lastSyncAt'] as String?),
lastAttemptAt: _tryParseDate(json['lastAttemptAt'] as String?),
lastError: json['lastError'] as String?,
lastRejected: (json['lastRejected'] as List<dynamic>? ?? <dynamic>[])
.map(
(dynamic item) =>
MutationRejection.fromJson(item as Map<String, dynamic>),
)
.toList(growable: false),
);
}
final String? cursor;
final List<ChangeEnvelope> pendingChanges;
final DateTime? lastSyncAt;
final DateTime? lastAttemptAt;
final String? lastError;
final List<MutationRejection> lastRejected;
static const Object _unset = Object();
static const SyncState empty = SyncState(
pendingChanges: <ChangeEnvelope>[],
lastRejected: <MutationRejection>[],
);
SyncState copyWith({
Object? cursor = _unset,
List<ChangeEnvelope>? pendingChanges,
Object? lastSyncAt = _unset,
Object? lastAttemptAt = _unset,
Object? lastError = _unset,
List<MutationRejection>? lastRejected,
}) {
return SyncState(
cursor: identical(cursor, _unset) ? this.cursor : cursor as String?,
pendingChanges: pendingChanges ?? this.pendingChanges,
lastSyncAt: identical(lastSyncAt, _unset)
? this.lastSyncAt
: lastSyncAt as DateTime?,
lastAttemptAt: identical(lastAttemptAt, _unset)
? this.lastAttemptAt
: lastAttemptAt as DateTime?,
lastError: identical(lastError, _unset)
? this.lastError
: lastError as String?,
lastRejected: lastRejected ?? this.lastRejected,
);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
'cursor': cursor,
'pendingChanges': pendingChanges
.map((ChangeEnvelope change) => change.toJson())
.toList(growable: false),
'lastSyncAt': lastSyncAt?.toUtc().toIso8601String(),
'lastAttemptAt': lastAttemptAt?.toUtc().toIso8601String(),
'lastError': lastError,
'lastRejected': lastRejected
.map((MutationRejection rejection) => rejection.toJson())
.toList(growable: false),
};
}
static DateTime? _tryParseDate(String? raw) {
if (raw == null || raw.isEmpty) {
return null;
}
final DateTime? parsed = DateTime.tryParse(raw);
return parsed?.toLocal();
}
}