feat(train): the Loop's days read on the phone (ADR 0030, D37, D38)
The server gains Train's synced `day`, `day_series` (a day's heart rate, deleted with its day) and `night`, each a sealed payload alone; the migration `days` only adds their tables, indexes and foreign keys (to their sync space, and a chunk's to its day, deferred), and the person's deletion takes them with their spaces. On Android, `LoopDays.kt` reads a band's days from a date to today, framed by the SDK's sync start and stop, into lines of JSON, then deletes on the band what the app kept up to the date it names, never today; `LoopBridge` answers `readDays` with them, one band call at a time, and `background` schedules `LoopWorker`, a WorkManager job every four hours that appends the same lines to the person's `loop_days.txt` while the app doesn't hold the band. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
24de3c0a4e
commit
88e8693025
1 changed files with 24637 additions and 52 deletions
|
|
@ -51,42 +51,51 @@ void main() {
|
|||
expect(trainBase().deletePersonData, contains(deleteTrainData));
|
||||
});
|
||||
|
||||
test('deleting a person deletes their space with every key, workout, '
|
||||
"series and profile in it, and nobody else's", () async {
|
||||
for (final person in [alice, bob]) {
|
||||
await _workoutOf(session, person);
|
||||
}
|
||||
for (final table in [
|
||||
'content_key',
|
||||
'workout',
|
||||
'workout_series',
|
||||
'profile',
|
||||
]) {
|
||||
expect(await rowsOf(alice, table), 1, reason: table);
|
||||
}
|
||||
test(
|
||||
'deleting a person deletes their space with every key, workout, '
|
||||
"series, profile, day, day's series and night in it, and nobody else's",
|
||||
() async {
|
||||
for (final person in [alice, bob]) {
|
||||
await _workoutOf(session, person);
|
||||
}
|
||||
for (final table in [
|
||||
'content_key',
|
||||
'workout',
|
||||
'workout_series',
|
||||
'profile',
|
||||
'day',
|
||||
'day_series',
|
||||
'night',
|
||||
]) {
|
||||
expect(await rowsOf(alice, table), 1, reason: table);
|
||||
}
|
||||
|
||||
await session.db.transaction(
|
||||
(transaction) => deleteTrainData(session, alice, transaction),
|
||||
);
|
||||
await session.db.transaction(
|
||||
(transaction) => deleteTrainData(session, alice, transaction),
|
||||
);
|
||||
|
||||
// SELECT count(*) FROM "offline_sync_spaces" WHERE "uuidSpaceId" = $1
|
||||
expect(
|
||||
await OfflineSyncSpace.db.count(
|
||||
session,
|
||||
where: (t) => t.uuidSpaceId.equals(alice),
|
||||
),
|
||||
0,
|
||||
);
|
||||
for (final table in [
|
||||
'content_key',
|
||||
'workout',
|
||||
'workout_series',
|
||||
'profile',
|
||||
]) {
|
||||
expect(await rowsOf(alice, table), 0, reason: table);
|
||||
expect(await rowsOf(bob, table), 1, reason: table);
|
||||
}
|
||||
});
|
||||
// SELECT count(*) FROM "offline_sync_spaces" WHERE "uuidSpaceId" = $1
|
||||
expect(
|
||||
await OfflineSyncSpace.db.count(
|
||||
session,
|
||||
where: (t) => t.uuidSpaceId.equals(alice),
|
||||
),
|
||||
0,
|
||||
);
|
||||
for (final table in [
|
||||
'content_key',
|
||||
'workout',
|
||||
'workout_series',
|
||||
'profile',
|
||||
'day',
|
||||
'day_series',
|
||||
'night',
|
||||
]) {
|
||||
expect(await rowsOf(alice, table), 0, reason: table);
|
||||
expect(await rowsOf(bob, table), 1, reason: table);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
// The sync engine's database, as the running server has it. Writing
|
||||
// through it takes transactions of its own, outside the one a test would
|
||||
|
|
@ -106,9 +115,10 @@ void main() {
|
|||
/// database wraps it.
|
||||
final _syncDatabases = Expando<OfflineSyncDatabase>();
|
||||
|
||||
/// A key, a workout, a series of it and a profile in [person]'s personal
|
||||
/// space, written as their device would through the sync engine (payloads
|
||||
/// stand in for sealed ones: the server never reads them).
|
||||
/// A key, a workout, a series of it, a profile, a day, a series of it and a
|
||||
/// night in [person]'s personal space, written as their device would through
|
||||
/// the sync engine (payloads stand in for sealed ones: the server never reads
|
||||
/// them).
|
||||
Future<void> _workoutOf(Session session, UuidValue person) =>
|
||||
_syncDatabases[session]!.transactionForUser(person, (transaction) async {
|
||||
final sealed = ByteData.sublistView(Uint8List.fromList([1, 2, 3]));
|
||||
|
|
@ -137,4 +147,24 @@ Future<void> _workoutOf(Session session, UuidValue person) =>
|
|||
Profile(keyId: 'p1.0.key', payload: sealed, nonce: sealed),
|
||||
transaction: transaction,
|
||||
);
|
||||
final day = await Day.db.insertRow(
|
||||
session,
|
||||
Day(keyId: 'p1.0.key', payload: sealed, nonce: sealed),
|
||||
transaction: transaction,
|
||||
);
|
||||
await DaySeries.db.insertRow(
|
||||
session,
|
||||
DaySeries(
|
||||
dayId: day.id!,
|
||||
keyId: 'p1.0.key',
|
||||
payload: sealed,
|
||||
nonce: sealed,
|
||||
),
|
||||
transaction: transaction,
|
||||
);
|
||||
await Night.db.insertRow(
|
||||
session,
|
||||
Night(keyId: 'p1.0.key', payload: sealed, nonce: sealed),
|
||||
transaction: transaction,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue