fix(watch): finalise correctif sync workoutHistory/exercise et distance live montre (#157)

This commit is contained in:
2026-07-29 11:22:17 +02:00
parent 6f913e4e8d
commit 30c6259748
28 changed files with 1658 additions and 248 deletions

View File

@ -492,10 +492,14 @@ final class DriftLocalSyncChangeRepository
switch (item.resourceType) {
case SyncResourceType.exercise:
final exercise = _exerciseFromPayload(item);
await database
.into(database.exercises)
.insertOnConflictUpdate(_exerciseCompanion(exercise));
await _writeExerciseStarterMetadata(database, exercise);
await database.transaction(() async {
await database
.into(database.exercises)
.insertOnConflictUpdate(_exerciseCompanion(exercise));
await _writeExerciseStarterMetadata(database, exercise);
await _replaceRemoteExerciseImages(exercise);
await _replaceRemoteExerciseSteps(exercise);
});
return true;
case SyncResourceType.mediaAsset:
await database
@ -542,7 +546,11 @@ final class DriftLocalSyncChangeRepository
});
return true;
case SyncResourceType.workoutHistory:
return false;
await _replaceRemoteWorkoutHistory(
_workoutHistoryFromLocalBackupPayload(item),
item.clientUpdatedAt,
);
return true;
}
}
@ -604,7 +612,7 @@ final class DriftLocalSyncChangeRepository
}
return _LocalSyncSnapshot.fromMetadata(
history.metadata,
_workoutHistoryPayload(history),
_localWorkoutHistoryPayload(history),
);
}
@ -627,6 +635,212 @@ final class DriftLocalSyncChangeRepository
payload: {'id': id},
);
}
Future<void> _replaceRemoteWorkoutHistory(
domain.WorkoutHistory history,
DateTime deletedAt,
) async {
await database.transaction(() async {
await database
.into(database.workoutHistories)
.insertOnConflictUpdate(_workoutHistoryCompanion(history));
for (final result in history.results) {
await database
.into(database.workoutHistorySetResults)
.insertOnConflictUpdate(_workoutHistorySetResultCompanion(result));
}
for (final result in history.stepResults) {
await database
.into(database.workoutHistoryStepResults)
.insertOnConflictUpdate(_workoutHistoryStepResultCompanion(result));
}
final activeResultIds = history.results
.map((result) => result.metadata.id)
.toSet();
await _softDeleteRemoteWorkoutHistoryChildren(
tableName: 'workout_history_set_results',
historyId: history.metadata.id,
keepIds: activeResultIds,
deletedAt: deletedAt,
);
final activeStepResultIds = history.stepResults
.map((result) => result.metadata.id)
.toSet();
await _softDeleteRemoteWorkoutHistoryChildren(
tableName: 'workout_history_step_results',
historyId: history.metadata.id,
keepIds: activeStepResultIds,
deletedAt: deletedAt,
);
});
}
Future<void> _softDeleteRemoteWorkoutHistoryChildren({
required String tableName,
required String historyId,
required Set<String> keepIds,
required DateTime deletedAt,
}) async {
final keepPredicate = keepIds.isEmpty
? ''
: 'AND id NOT IN (${List.filled(keepIds.length, '?').join(', ')})';
await database.customUpdate(
'''
UPDATE $tableName
SET deleted_at = ?, updated_at = ?, sync_state = 'deleted'
WHERE workout_history_id = ?
AND deleted_at IS NULL
$keepPredicate
''',
variables: [
Variable<DateTime>(deletedAt.toUtc()),
Variable<DateTime>(deletedAt.toUtc()),
Variable<String>(historyId),
for (final id in keepIds) Variable<String>(id),
],
);
}
Future<void> _replaceRemoteExerciseImages(domain.Exercise exercise) async {
final rows = await (database.select(
database.exerciseImages,
)..where((table) => table.exerciseId.equals(exercise.metadata.id))).get();
final activeRowsByMediaId = {
for (final row in rows)
if (row.deletedAt == null) row.mediaAssetId: row,
};
final desiredIds = exercise.imageMediaIds.toSet();
final removedIds = activeRowsByMediaId.values
.where((row) => !desiredIds.contains(row.mediaAssetId))
.map((row) => row.id)
.toList();
await _softDeleteRemoteRows(
tableName: 'exercise_images',
ids: removedIds,
deletedAt: exercise.metadata.updatedAt,
);
for (var index = 0; index < exercise.imageMediaIds.length; index++) {
final mediaId = exercise.imageMediaIds[index];
final existing = activeRowsByMediaId[mediaId];
final metadata = existing == null
? domain.EntityMetadata(
id: _exerciseImageId(exercise.metadata.id, mediaId),
createdAt: exercise.metadata.updatedAt,
updatedAt: exercise.metadata.updatedAt,
syncState: exercise.metadata.syncState,
originDeviceId: exercise.metadata.originDeviceId,
)
: _metadataFromRow(existing).touch(exercise.metadata.updatedAt);
await database
.into(database.exerciseImages)
.insertOnConflictUpdate(
db.ExerciseImagesCompanion(
id: Value(metadata.id),
createdAt: Value(metadata.createdAt.toUtc()),
updatedAt: Value(metadata.updatedAt.toUtc()),
deletedAt: Value(_utcOrNull(metadata.deletedAt)),
schemaVersion: Value(metadata.schemaVersion),
syncState: Value(_syncStateToDb(metadata.syncState)),
localRevision: Value(metadata.localRevision),
originDeviceId: Value(metadata.originDeviceId),
futureOwnerProfileId: Value(metadata.futureOwnerProfileId),
lastSyncedAt: Value(_utcOrNull(metadata.lastSyncedAt)),
remoteRevision: Value(metadata.remoteRevision),
exerciseId: Value(exercise.metadata.id),
mediaAssetId: Value(mediaId),
position: Value(index),
),
);
}
}
Future<void> _replaceRemoteExerciseSteps(domain.Exercise exercise) async {
final rows = await (database.select(
database.exerciseSteps,
)..where((table) => table.exerciseId.equals(exercise.metadata.id))).get();
final activeRowsByStepId = {
for (final row in rows)
if (row.deletedAt == null) row.id: row,
};
final desiredIds = exercise.steps.map((step) => step.id).toSet();
final removedIds = activeRowsByStepId.values
.where((row) => !desiredIds.contains(row.id))
.map((row) => row.id)
.toList();
await _softDeleteRemoteRows(
tableName: 'exercise_steps',
ids: removedIds,
deletedAt: exercise.metadata.updatedAt,
);
for (final step in exercise.steps) {
final existing = activeRowsByStepId[step.id];
final metadata = existing == null
? domain.EntityMetadata(
id: step.id,
createdAt: exercise.metadata.updatedAt,
updatedAt: exercise.metadata.updatedAt,
syncState: exercise.metadata.syncState,
originDeviceId: exercise.metadata.originDeviceId,
)
: _metadataFromRow(existing).touch(exercise.metadata.updatedAt);
await database
.into(database.exerciseSteps)
.insertOnConflictUpdate(
db.ExerciseStepsCompanion(
id: Value(metadata.id),
createdAt: Value(metadata.createdAt.toUtc()),
updatedAt: Value(metadata.updatedAt.toUtc()),
deletedAt: Value(_utcOrNull(metadata.deletedAt)),
schemaVersion: Value(metadata.schemaVersion),
syncState: Value(_syncStateToDb(metadata.syncState)),
localRevision: Value(metadata.localRevision),
originDeviceId: Value(metadata.originDeviceId),
futureOwnerProfileId: Value(metadata.futureOwnerProfileId),
lastSyncedAt: Value(_utcOrNull(metadata.lastSyncedAt)),
remoteRevision: Value(metadata.remoteRevision),
exerciseId: Value(exercise.metadata.id),
position: Value(step.position),
name: Value(step.name),
type: Value(_exerciseStepTypeToDb(step.type)),
defaultTargetValue: Value(step.defaultTargetValue),
hasScore: Value(step.hasScore),
scoreInputMode: Value(
step.hasScore ? _scoreInputModeToDb(step.scoreInputMode) : null,
),
scoreLabel: Value(step.scoreLabel),
scoreUnit: Value(step.scoreUnit),
defaultTargetScore: Value(step.defaultTargetScore),
defaultTargetScoreTimeMs: Value(step.defaultTargetScoreTimeMs),
linkedToSeriesScore: Value(step.linkedToSeriesScore),
),
);
}
}
Future<void> _softDeleteRemoteRows({
required String tableName,
required List<String> ids,
required DateTime deletedAt,
}) async {
if (ids.isEmpty) {
return;
}
await database.customUpdate(
'''
UPDATE $tableName
SET deleted_at = ?, updated_at = ?, sync_state = 'deleted'
WHERE id IN (${List.filled(ids.length, '?').join(', ')})
''',
variables: [
Variable<DateTime>(deletedAt.toUtc()),
Variable<DateTime>(deletedAt.toUtc()),
for (final id in ids) Variable<String>(id),
],
);
}
}
final class DriftShareInboxRepository implements ShareInboxRepository {