refactor(app): studysets and settings are written in the newest schema

Decided 2026-10-07: the codecs write the newest schema every time, as
before, rather than the oldest one that holds what is there. An older
app loses sight of a studyset or of the settings once a newer one has
written them, until it is updated too.

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-07 10:21:58 +02:00
commit ad28a5d8c2
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
4 changed files with 19 additions and 26 deletions

View file

@ -19,22 +19,17 @@ import 'package:halkyon_learn_flutter/src/feature/tag/model/tag.dart';
// layout (the widgets' order and those hidden, by name). Schema 4 added the
// Pomodoro timer's profiles and the one chosen, left out while as they come.
// Older schemas read with what they lack as it comes.
//
// Settings whose Pomodoro profiles are as they come are still written in
// schema 3: an app that knows only schema 3 skips a schema 4 payload whole,
// and would lose every setting.
/// The newest schema this app writes.
/// The schema this app writes.
const settingsSchemaVersion = 4;
/// Encodes [settings] into their payload, in the oldest schema that holds
/// them.
/// Encodes [settings] into their payload.
ByteData encodeSettingsPayload(PersonSettings settings) {
const defaults = StudyOptions();
final study = settings.study;
final pomodoro = settings.pomodoro;
return encodePayload(
pomodoro == const PomodoroSettings() ? 3 : settingsSchemaVersion,
settingsSchemaVersion,
{
'studyTags': {
for (final MapEntry(key: studysetId, value: tags)

View file

@ -13,18 +13,13 @@ import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_conten
// without them. Schema 3 added the study options (`study`: new cards and
// reviews a day, 0 for no limit, and the order), left out when the studyset
// has none.
//
// A studyset without study options is still written in schema 2: an app that
// knows only schema 2 skips a schema 3 payload whole, and would lose sight of
// every studyset edited on a newer app otherwise.
/// The newest schema this app writes.
/// The schema this app writes.
const studysetSchemaVersion = 3;
/// Encodes a studyset's content into its payload, in the oldest schema that
/// holds it.
/// Encodes a studyset's content into its payload.
ByteData encodeStudysetPayload(StudysetContent content) => encodePayload(
content.studyOptions == null ? 2 : studysetSchemaVersion,
studysetSchemaVersion,
{
'title': content.title,
'description': content.description.toJson(),

View file

@ -181,11 +181,14 @@ void main() {
expect(decodeSettingsPayload(payload), settings);
});
test('as they come, are written in the third schema, which older apps '
'read', () {
final payload = encodeSettingsPayload(const PersonSettings());
test('as they come, are left out, in the fourth schema all the same', () {
final payload = decodePayload(
encodeSettingsPayload(const PersonSettings()),
newestSchema: 4,
);
expect(decodePayload(payload, newestSchema: 4)['schemaVersion'], 3);
expect(payload['schemaVersion'], 4);
expect(payload.keys, isNot(contains('pomodoro')));
});
test('that are malformed read as they come, keeping the other '

View file

@ -70,13 +70,13 @@ void main() {
expect(json(payload)['schemaVersion'], studysetSchemaVersion);
});
test('without study options still writes the second schema, which '
'older apps read', () {
test('without study options writes the third schema all the same', () {
final payload = encodeStudysetPayload(
StudysetContent(title: 'Japanese', description: RichContent.empty()),
);
expect(json(payload)['schemaVersion'], 2);
expect(json(payload)['schemaVersion'], 3);
expect(json(payload).keys, isNot(contains('study')));
});
test('keeps the study options', () {