Ajoute la synchronisation client incrémentale LWW vers l'API serveur (application/use_cases.dart: SyncUseCases, ports.dart, migration Drift schemaVersion 10→11 pour les métadonnées de sync et mappings de ressources, infrastructure/remote/sync_api.dart) et l'écran de profil avec entrée sur l'accueil (presentation/profile_screen.dart, home_screen.dart, presentation.dart). Développés dans le même worktree partagé par DevBackend et DevFrontend ; commit combiné car test/presentation/home_screen_test.dart mélange authentiquement les deux tickets (le test de l'entrée Profil et la mise à jour du fake de bootstrap requise par le nouveau getter syncUseCases sur AppDependencies). Corrige au passage une couleur `crimson` inexistante dans le thème (remplacée par colorScheme.error) et une signature de paramètres positionnels/nommés incohérente sur un fake de test. flutter pub get OK, build_runner OK, dart format appliqué, analyze propre (mêmes infos préexistantes), 119/119 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
172 lines
5.1 KiB
Dart
172 lines
5.1 KiB
Dart
import '../../application/application.dart';
|
|
import 'http_api_client.dart';
|
|
|
|
final class HttpRemoteSyncApi implements RemoteSyncApi {
|
|
const HttpRemoteSyncApi(this.client);
|
|
|
|
final HttpApiClient client;
|
|
|
|
@override
|
|
Future<RemoteSyncPushResult> push({
|
|
required String deviceId,
|
|
required List<RemoteSyncPushItem> items,
|
|
required String token,
|
|
}) async {
|
|
final response = await client.postJson(
|
|
'/sync/push',
|
|
bearerToken: token,
|
|
body: {
|
|
'deviceId': deviceId,
|
|
'items': items.map(_pushItemToJson).toList(),
|
|
},
|
|
);
|
|
return RemoteSyncPushResult(
|
|
serverCursor: _optionalString(response, 'serverCursor'),
|
|
results: (_list(
|
|
response,
|
|
'results',
|
|
)).map((item) => _pushItemResultFromJson(_map(item))).toList(),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<RemoteSyncPullResult> pull({
|
|
required String? since,
|
|
required String token,
|
|
}) async {
|
|
final response = await client.getJson(
|
|
'/sync/pull',
|
|
bearerToken: token,
|
|
queryParameters: {'since': since},
|
|
);
|
|
return RemoteSyncPullResult(
|
|
serverCursor: _optionalString(response, 'serverCursor'),
|
|
items: (_list(
|
|
response,
|
|
'items',
|
|
)).map((item) => _syncedItemFromJson(_map(item))).toList(),
|
|
);
|
|
}
|
|
|
|
Map<String, Object?> _pushItemToJson(RemoteSyncPushItem item) => {
|
|
'resourceType': _resourceTypeToWire(item.resourceType),
|
|
'clientId': item.clientId,
|
|
'schemaVersion': item.schemaVersion,
|
|
'clientUpdatedAt': item.clientUpdatedAt.toUtc().toIso8601String(),
|
|
'deletedAt': item.deletedAt?.toUtc().toIso8601String(),
|
|
'payload': item.payload,
|
|
};
|
|
|
|
RemoteSyncPushItemResult _pushItemResultFromJson(Map<String, Object?> json) {
|
|
return RemoteSyncPushItemResult(
|
|
resourceType: _resourceTypeFromWire(
|
|
_requiredString(json, 'resourceType'),
|
|
),
|
|
clientId: _requiredString(json, 'clientId'),
|
|
serverId: _optionalString(json, 'serverId'),
|
|
status: _pushStatusFromWire(_requiredString(json, 'status')),
|
|
serverUpdatedAt: _optionalDateTime(json, 'serverUpdatedAt'),
|
|
errorMessage: _optionalString(json, 'message'),
|
|
);
|
|
}
|
|
|
|
RemoteSyncedItem _syncedItemFromJson(Map<String, Object?> json) {
|
|
return RemoteSyncedItem(
|
|
resourceType: _resourceTypeFromWire(
|
|
_requiredString(json, 'resourceType'),
|
|
),
|
|
clientId: _requiredString(json, 'clientId'),
|
|
serverId: _requiredString(json, 'serverId'),
|
|
schemaVersion: _requiredInt(json, 'schemaVersion'),
|
|
clientUpdatedAt: _requiredDateTime(json, 'clientUpdatedAt'),
|
|
serverUpdatedAt: _requiredDateTime(json, 'serverUpdatedAt'),
|
|
deletedAt: _optionalDateTime(json, 'deletedAt'),
|
|
payload: _map(json['payload']),
|
|
);
|
|
}
|
|
}
|
|
|
|
String _resourceTypeToWire(SyncResourceType type) => switch (type) {
|
|
SyncResourceType.exercise => 'exercise',
|
|
SyncResourceType.program => 'program',
|
|
SyncResourceType.workoutTemplate => 'workoutTemplate',
|
|
SyncResourceType.workoutHistory => 'workoutHistory',
|
|
SyncResourceType.mediaAsset => 'mediaAsset',
|
|
};
|
|
|
|
SyncResourceType _resourceTypeFromWire(String value) => switch (value) {
|
|
'exercise' => SyncResourceType.exercise,
|
|
'program' => SyncResourceType.program,
|
|
'workoutTemplate' => SyncResourceType.workoutTemplate,
|
|
'workoutHistory' => SyncResourceType.workoutHistory,
|
|
'mediaAsset' => SyncResourceType.mediaAsset,
|
|
_ => throw RemoteAuthException(
|
|
RemoteAuthFailure.unknown,
|
|
'Unknown sync resource type: $value',
|
|
),
|
|
};
|
|
|
|
RemoteSyncPushStatus _pushStatusFromWire(String value) => switch (value) {
|
|
'accepted' => RemoteSyncPushStatus.accepted,
|
|
'ignoredOlder' => RemoteSyncPushStatus.ignoredOlder,
|
|
'error' => RemoteSyncPushStatus.error,
|
|
_ => throw RemoteAuthException(
|
|
RemoteAuthFailure.unknown,
|
|
'Unknown sync push status: $value',
|
|
),
|
|
};
|
|
|
|
List<Object?> _list(Map<String, Object?> json, String key) {
|
|
final value = json[key];
|
|
if (value is List) {
|
|
return value.cast<Object?>();
|
|
}
|
|
return const [];
|
|
}
|
|
|
|
Map<String, Object?> _map(Object? value) {
|
|
if (value is Map) {
|
|
return Map<String, Object?>.from(value);
|
|
}
|
|
throw const RemoteAuthException(
|
|
RemoteAuthFailure.unknown,
|
|
'Expected JSON object.',
|
|
);
|
|
}
|
|
|
|
String _requiredString(Map<String, Object?> json, String key) {
|
|
final value = json[key];
|
|
if (value is String && value.trim().isNotEmpty) {
|
|
return value;
|
|
}
|
|
throw RemoteAuthException(
|
|
RemoteAuthFailure.unknown,
|
|
'$key is missing from sync response.',
|
|
);
|
|
}
|
|
|
|
String? _optionalString(Map<String, Object?> json, String key) {
|
|
final value = json[key];
|
|
return value is String && value.trim().isNotEmpty ? value : null;
|
|
}
|
|
|
|
int _requiredInt(Map<String, Object?> json, String key) {
|
|
final value = json[key];
|
|
if (value is int) {
|
|
return value;
|
|
}
|
|
throw RemoteAuthException(
|
|
RemoteAuthFailure.unknown,
|
|
'$key is missing from sync response.',
|
|
);
|
|
}
|
|
|
|
DateTime _requiredDateTime(Map<String, Object?> json, String key) {
|
|
return DateTime.parse(_requiredString(json, key)).toUtc();
|
|
}
|
|
|
|
DateTime? _optionalDateTime(Map<String, Object?> json, String key) {
|
|
final value = _optionalString(json, key);
|
|
return value == null ? null : DateTime.parse(value).toUtc();
|
|
}
|