feat(train): the person's workouts, sealed and synced (ADR 0030, D14)

WorkoutRepository keeps a workout with its series, sealed with the
person's key: a workout for its row, a series chunk for its row and its
workout, so the instance can neither read them nor move a chunk to
another workout. A readable or changed row shows locked, never as the
person's; a device without the account's keys saves nothing. On the
device's database a workout and its series are written in one
transaction, and a deleted one is blanked before it goes, so the sync
engine's tombstones hold nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-28 11:24:20 +01:00
commit d1daec9c0b
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -0,0 +1,58 @@
import 'dart:async';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_train_flutter/src/feature/workout/data/workout_gateway.dart';
/// [WorkoutGateway] in memory, whose rows a test changes as an instance
/// could; it records the envelopes written, so a test sees what reached it.
/// Blanking before a delete is the sync gateway's.
class MemoryWorkoutGateway() implements WorkoutGateway {
final workoutRows = <String, Envelope>{};
final seriesRows = <SeriesRow>[];
/// Every envelope ever written, in order.
final written = <Envelope>[];
final _remote = StreamController<void>.broadcast();
/// As a sync bringing others' changes.
void bringRemoteChanges() => _remote.add(null);
@override
Future<List<WorkoutRow>> workouts() async => [
for (final MapEntry(:key, :value) in workoutRows.entries)
(id: key, envelope: value),
];
@override
Future<List<SeriesRow>> seriesOf(String workoutId) async => [
for (final row in seriesRows)
if (row.workoutId == workoutId) row,
];
@override
Future<void> insert(WorkoutRow workout, List<SeriesRow> series) async {
workoutRows[workout.id] = workout.envelope;
seriesRows.addAll(series);
written
..add(workout.envelope)
..addAll(series.map((row) => row.envelope));
}
@override
Future<void> update(String id, Envelope envelope) async {
if (workoutRows.containsKey(id)) {
workoutRows[id] = envelope;
written.add(envelope);
}
}
@override
Future<void> delete(String id) async {
seriesRows.removeWhere((row) => row.workoutId == id);
workoutRows.remove(id);
}
@override
Stream<void> get remoteChanges => _remote.stream;
}