halkyon/halkyon_learn_flutter/test/sync/support/sync_devices.dart

247 lines
8.6 KiB
Dart
Raw Normal View History

feat(app): flashcards on append-only revisions A studyset's screen lists its cards (reorderable; a marker on cards changed on two devices) and opens a card's screen with an editor for each side. Saving appends a revision whose parents are all current heads. mergeRevisions merges several heads per side against their newest common ancestor, as decided: different sides merge automatically, the same side changed differently is a conflict. The card screen then shows every version of that side with "Keep this", and saving is possible once each conflicted side is settled, which resolves the conflict for every device. Nothing is merged silently inside a side. CardRepository watches a studyset's cards, creates at the end and moves by fractional index (fractional_indexing_dart, checked against the reference algorithm's test vectors), and deletes by blanking every revision first (ADR 0002, rule 2). Deleting a studyset now deletes its cards the same way before the studyset goes. Every person gets a CardRepository whose revisions name them as author. RichContent gains plainText for the list's previews. Tests: the merge (chains, different and same sides, conflicts, resolving, three heads, missing parents, arrival order), the payload, the repository against a fake gateway, both blocs, and the widgets, including that cancelling a delete keeps the card. The sync tests, which need the running server, cover merging and conflicts across two devices and blank revisions on the server; their device helper is now shared in test/sync/support. AGENTS.md describes the revision pattern, the fractional order, and two tooling traps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 14:41:44 +02:00
import 'dart:io';
import 'package:halkyon_learn_client/halkyon_learn_client.dart' as protocol;
Import Anki decks with their files and history Files in content: the person's images and sounds as asset rows synced like all content, their files kept by hash on each device, uploaded after each sync, downloaded on demand and checked against their hash (core/asset/AssetRepository). Rich text names them as asset:<id> URLs, in the editor's image block and an audio block of ours; any other URL is never fetched. Sounds play through audioplayers, one at a time. Content holding only an image or a sound isn't blank, and lists show it as such. The Anki import ("Import from Anki" under Add material): .apkg and .colpkg packages in all three formats Anki has written, read on an isolate of their own. Modern packages compress everything with zstd in frames that don't record their size, which pure_zstd v1.0.0 refuses; our fork decodes them. Fields become rich text by a converter of our own (lines, lists, formatting, images, sounds, cloze deletions hidden on the front). One card per note, the files first, studysets as chosen (flatten, sub-studysets, or tags by subdeck), the notes' tags, and, if wanted, the review history, which FSRS replays: the first template's reviews to the card's schedule, a reversed note type's second to the reverse one. Imported cards say where they came from in their first revision (card schema 3); the dashboard doesn't count them as written. The test fixtures are exported by Anki itself (tool/anki/make_fixtures.py). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-02 04:52:02 +02:00
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_file_cache.dart';
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart';
import 'package:halkyon_learn_flutter/src/core/asset/data/serverpod_asset_server_gateway.dart';
import 'package:halkyon_learn_flutter/src/core/asset/data/sync_asset_gateway.dart';
feat(app): flashcards on append-only revisions A studyset's screen lists its cards (reorderable; a marker on cards changed on two devices) and opens a card's screen with an editor for each side. Saving appends a revision whose parents are all current heads. mergeRevisions merges several heads per side against their newest common ancestor, as decided: different sides merge automatically, the same side changed differently is a conflict. The card screen then shows every version of that side with "Keep this", and saving is possible once each conflicted side is settled, which resolves the conflict for every device. Nothing is merged silently inside a side. CardRepository watches a studyset's cards, creates at the end and moves by fractional index (fractional_indexing_dart, checked against the reference algorithm's test vectors), and deletes by blanking every revision first (ADR 0002, rule 2). Deleting a studyset now deletes its cards the same way before the studyset goes. Every person gets a CardRepository whose revisions name them as author. RichContent gains plainText for the list's previews. Tests: the merge (chains, different and same sides, conflicts, resolving, three heads, missing parents, arrival order), the payload, the repository against a fake gateway, both blocs, and the widgets, including that cancelling a delete keeps the card. The sync tests, which need the running server, cover merging and conflicts across two devices and blank revisions on the server; their device helper is now shared in test/sync/support. AGENTS.md describes the revision pattern, the fractional order, and two tooling traps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 14:41:44 +02:00
import 'package:halkyon_learn_flutter/src/core/sync/sync_service.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/sync_card_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/card/model/card.dart';
feat: notes in studysets, synced and merged as cards are, with tags Server: three synced tables, only additions (migration 20261002191122122-notes): `note` (its studyset, a fractional position), `note_revision` (append-only, as a card's: parents, author, report, the payload {schemaVersion, title, body}) and `note_tag` (one row per assignment, never updated, as `card_tag`). Each indexes `spaceId`, and everything goes with its space, studyset, note or tag. A migration of synced tables locks app builds from before it out of syncing (ADR 0002, rule 3): cheap now, before the first release. App, `feature/note`: - `NoteContent` is a title and rich text, merged apart (ADR 0003): a title changed on one device and the body on another both survive, a body changed on two is a conflict. - `NoteRepository` creates a note at the end of its studyset, saves an edit on the revisions it started from (`saveEdit`, as the card composer now does), says when a note was last edited (its newest head's UUID v7), counts notes per studyset, and deletes a note's revisions blanked first (ADR 0002, rule 2). - Notes share the person's tags: `setTagsOfNote`, and folding tags of one name or undoing a tag's deletion covers notes too. - The delayed deletion with its Undo moves to `core/async` (`DelayedDeletions`): `StudysetDeletions` builds on it and now takes a studyset's notes too; `NoteDeletions` deletes a single note. Tests: content, merge, payload, repository, deletions, tags, the device migration guard, and sync tests against the server (four). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-02 21:53:56 +02:00
import 'package:halkyon_learn_flutter/src/feature/note/data/note_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/note/data/sync_note_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/note/model/note.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/data/sync_settings_gateway.dart';
feat(app): study with FSRS A studyset's card list gains "Study". The study screen shows the front, then the answer and the four ratings, each labelled with when the card would come back; every rating is recorded with how long answering took, and the queue is worked out anew after each. Schedules are never stored: scheduleOf replays a card's reviews in time order with FSRS-6 (fsrs v2.0.1, pinned exactly so every device on one app version computes the same). The package's fuzz can only be seeded through an API meant for tests, and due dates don't feed back into a card's state, so the replay runs without fuzz and fuzzedDays spreads the final due date over FSRS's range, picked by a stable FNV-1a fraction of the card's id and review count. studyQueue takes due cards earliest first, then new cards in studyset order, 20 per studyset and local day, and brings a learning card due within 20 minutes early when nothing else is due. Deleting a card deletes its reviews. Tests: the replay (learning, growing intervals, the same schedule whatever order reviews arrive in, previews matching what a rating yields), the fuzz range, the queue (order, the daily limit counted per local day, learn-ahead), the repository, the session bloc under a controlled clock, the screen, and a sync test for ratings on two devices. AGENTS.md records the review log and scheduling, and how plugin lint rules are ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 16:59:07 +02:00
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/study/data/sync_review_gateway.dart';
feat(app): flashcards on append-only revisions A studyset's screen lists its cards (reorderable; a marker on cards changed on two devices) and opens a card's screen with an editor for each side. Saving appends a revision whose parents are all current heads. mergeRevisions merges several heads per side against their newest common ancestor, as decided: different sides merge automatically, the same side changed differently is a conflict. The card screen then shows every version of that side with "Keep this", and saving is possible once each conflicted side is settled, which resolves the conflict for every device. Nothing is merged silently inside a side. CardRepository watches a studyset's cards, creates at the end and moves by fractional index (fractional_indexing_dart, checked against the reference algorithm's test vectors), and deletes by blanking every revision first (ADR 0002, rule 2). Deleting a studyset now deletes its cards the same way before the studyset goes. Every person gets a CardRepository whose revisions name them as author. RichContent gains plainText for the list's previews. Tests: the merge (chains, different and same sides, conflicts, resolving, three heads, missing parents, arrival order), the payload, the repository against a fake gateway, both blocs, and the widgets, including that cancelling a delete keeps the card. The sync tests, which need the running server, cover merging and conflicts across two devices and blank revisions on the server; their device helper is now shared in test/sync/support. AGENTS.md describes the revision pattern, the fractional order, and two tooling traps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 14:41:44 +02:00
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/sync_studyset_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
import 'package:halkyon_learn_flutter/src/feature/tag/data/sync_tag_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
feat(app): flashcards on append-only revisions A studyset's screen lists its cards (reorderable; a marker on cards changed on two devices) and opens a card's screen with an editor for each side. Saving appends a revision whose parents are all current heads. mergeRevisions merges several heads per side against their newest common ancestor, as decided: different sides merge automatically, the same side changed differently is a conflict. The card screen then shows every version of that side with "Keep this", and saving is possible once each conflicted side is settled, which resolves the conflict for every device. Nothing is merged silently inside a side. CardRepository watches a studyset's cards, creates at the end and moves by fractional index (fractional_indexing_dart, checked against the reference algorithm's test vectors), and deletes by blanking every revision first (ADR 0002, rule 2). Deleting a studyset now deletes its cards the same way before the studyset goes. Every person gets a CardRepository whose revisions name them as author. RichContent gains plainText for the list's previews. Tests: the merge (chains, different and same sides, conflicts, resolving, three heads, missing parents, arrival order), the payload, the repository against a fake gateway, both blocs, and the widgets, including that cancelling a delete keeps the card. The sync tests, which need the running server, cover merging and conflicts across two devices and blank revisions on the server; their device helper is now shared in test/sync/support. AGENTS.md describes the revision pattern, the fractional order, and two tooling traps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 14:41:44 +02:00
import 'package:postgres/postgres.dart' as pg;
import 'package:serverpod_auth_core_client/serverpod_auth_core_client.dart';
import 'package:serverpod_offline_sync_client/offline_sync.dart';
import 'package:yaml/yaml.dart';
// What the sync tests (ADR 0001, rule 9) share: devices of people signed in
// against the running development server, and a look at its database.
const _serverUrl = 'http://localhost:8080/';
/// A new anonymous person, as the app signs one in on first launch.
Future<AuthSuccess> signInAnonymously() async {
final client = protocol.Client(_serverUrl);
try {
return await client.anonymousIdp.login();
} finally {
client.close();
}
}
/// Runs [sql] on the running development server's database, as its operator
/// could.
///
/// `serverpod start` runs an embedded PostgreSQL. On Linux and macOS it
/// listens on a Unix socket in the server's `.serverpod/development/run` and
/// trusts local connections; elsewhere it listens on TCP as
/// `config/development.yaml` says, with the password from
feat(app): flashcards on append-only revisions A studyset's screen lists its cards (reorderable; a marker on cards changed on two devices) and opens a card's screen with an editor for each side. Saving appends a revision whose parents are all current heads. mergeRevisions merges several heads per side against their newest common ancestor, as decided: different sides merge automatically, the same side changed differently is a conflict. The card screen then shows every version of that side with "Keep this", and saving is possible once each conflicted side is settled, which resolves the conflict for every device. Nothing is merged silently inside a side. CardRepository watches a studyset's cards, creates at the end and moves by fractional index (fractional_indexing_dart, checked against the reference algorithm's test vectors), and deletes by blanking every revision first (ADR 0002, rule 2). Deleting a studyset now deletes its cards the same way before the studyset goes. Every person gets a CardRepository whose revisions name them as author. RichContent gains plainText for the list's previews. Tests: the merge (chains, different and same sides, conflicts, resolving, three heads, missing parents, arrival order), the payload, the repository against a fake gateway, both blocs, and the widgets, including that cancelling a delete keeps the card. The sync tests, which need the running server, cover merging and conflicts across two devices and blank revisions on the server; their device helper is now shared in test/sync/support. AGENTS.md describes the revision pattern, the fractional order, and two tooling traps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 14:41:44 +02:00
/// `config/passwords.yaml`.
Future<pg.Result> queryServer(
String sql,
Map<String, Object?> parameters,
) async {
final connection = await _openServerDatabase();
try {
return await connection.execute(pg.Sql.named(sql), parameters: parameters);
} finally {
await connection.close();
}
}
Future<pg.Connection> _openServerDatabase() async {
const server = '../halkyon_learn_server';
const settings = pg.ConnectionSettings(sslMode: pg.SslMode.disable);
final development = loadYaml(
File('$server/config/development.yaml').readAsStringSync(),
) as YamlMap;
feat(app): flashcards on append-only revisions A studyset's screen lists its cards (reorderable; a marker on cards changed on two devices) and opens a card's screen with an editor for each side. Saving appends a revision whose parents are all current heads. mergeRevisions merges several heads per side against their newest common ancestor, as decided: different sides merge automatically, the same side changed differently is a conflict. The card screen then shows every version of that side with "Keep this", and saving is possible once each conflicted side is settled, which resolves the conflict for every device. Nothing is merged silently inside a side. CardRepository watches a studyset's cards, creates at the end and moves by fractional index (fractional_indexing_dart, checked against the reference algorithm's test vectors), and deletes by blanking every revision first (ADR 0002, rule 2). Deleting a studyset now deletes its cards the same way before the studyset goes. Every person gets a CardRepository whose revisions name them as author. RichContent gains plainText for the list's previews. Tests: the merge (chains, different and same sides, conflicts, resolving, three heads, missing parents, arrival order), the payload, the repository against a fake gateway, both blocs, and the widgets, including that cancelling a delete keeps the card. The sync tests, which need the running server, cover merging and conflicts across two devices and blank revisions on the server; their device helper is now shared in test/sync/support. AGENTS.md describes the revision pattern, the fractional order, and two tooling traps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 14:41:44 +02:00
final database = development['database'] as YamlMap;
final name = database['name'] as String;
final user = database['user'] as String;
final socket = File(
'$server/.serverpod/development/run/.s.PGSQL.5432',
).absolute.path;
if (FileSystemEntity.typeSync(socket) ==
FileSystemEntityType.unixDomainSock) {
// A socket's path may be about 100 bytes at most, fewer than the
// project's depth needs; a link in /tmp leads there by a short path, and
// an open connection no longer needs it.
final shortcut = Directory('/tmp').createTempSync('halkyon-pg-');
try {
final link = Link('${shortcut.path}/s')..createSync(socket);
return await pg.Connection.open(
pg.Endpoint(
host: link.path,
isUnixSocket: true,
database: name,
username: user,
),
settings: settings,
);
} finally {
shortcut.deleteSync(recursive: true);
}
}
final passwords = loadYaml(
File('$server/config/passwords.yaml').readAsStringSync(),
) as YamlMap;
return await pg.Connection.open(
feat(app): flashcards on append-only revisions A studyset's screen lists its cards (reorderable; a marker on cards changed on two devices) and opens a card's screen with an editor for each side. Saving appends a revision whose parents are all current heads. mergeRevisions merges several heads per side against their newest common ancestor, as decided: different sides merge automatically, the same side changed differently is a conflict. The card screen then shows every version of that side with "Keep this", and saving is possible once each conflicted side is settled, which resolves the conflict for every device. Nothing is merged silently inside a side. CardRepository watches a studyset's cards, creates at the end and moves by fractional index (fractional_indexing_dart, checked against the reference algorithm's test vectors), and deletes by blanking every revision first (ADR 0002, rule 2). Deleting a studyset now deletes its cards the same way before the studyset goes. Every person gets a CardRepository whose revisions name them as author. RichContent gains plainText for the list's previews. Tests: the merge (chains, different and same sides, conflicts, resolving, three heads, missing parents, arrival order), the payload, the repository against a fake gateway, both blocs, and the widgets, including that cancelling a delete keeps the card. The sync tests, which need the running server, cover merging and conflicts across two devices and blank revisions on the server; their device helper is now shared in test/sync/support. AGENTS.md describes the revision pattern, the fractional order, and two tooling traps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 14:41:44 +02:00
pg.Endpoint(
host: database['host'] as String,
port: database['port'] as int,
database: name,
username: user,
feat(app): flashcards on append-only revisions A studyset's screen lists its cards (reorderable; a marker on cards changed on two devices) and opens a card's screen with an editor for each side. Saving appends a revision whose parents are all current heads. mergeRevisions merges several heads per side against their newest common ancestor, as decided: different sides merge automatically, the same side changed differently is a conflict. The card screen then shows every version of that side with "Keep this", and saving is possible once each conflicted side is settled, which resolves the conflict for every device. Nothing is merged silently inside a side. CardRepository watches a studyset's cards, creates at the end and moves by fractional index (fractional_indexing_dart, checked against the reference algorithm's test vectors), and deletes by blanking every revision first (ADR 0002, rule 2). Deleting a studyset now deletes its cards the same way before the studyset goes. Every person gets a CardRepository whose revisions name them as author. RichContent gains plainText for the list's previews. Tests: the merge (chains, different and same sides, conflicts, resolving, three heads, missing parents, arrival order), the payload, the repository against a fake gateway, both blocs, and the widgets, including that cancelling a delete keeps the card. The sync tests, which need the running server, cover merging and conflicts across two devices and blank revisions on the server; their device helper is now shared in test/sync/support. AGENTS.md describes the revision pattern, the fractional order, and two tooling traps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 14:41:44 +02:00
password: (passwords['development'] as YamlMap)['database'] as String,
),
settings: settings,
feat(app): flashcards on append-only revisions A studyset's screen lists its cards (reorderable; a marker on cards changed on two devices) and opens a card's screen with an editor for each side. Saving appends a revision whose parents are all current heads. mergeRevisions merges several heads per side against their newest common ancestor, as decided: different sides merge automatically, the same side changed differently is a conflict. The card screen then shows every version of that side with "Keep this", and saving is possible once each conflicted side is settled, which resolves the conflict for every device. Nothing is merged silently inside a side. CardRepository watches a studyset's cards, creates at the end and moves by fractional index (fractional_indexing_dart, checked against the reference algorithm's test vectors), and deletes by blanking every revision first (ADR 0002, rule 2). Deleting a studyset now deletes its cards the same way before the studyset goes. Every person gets a CardRepository whose revisions name them as author. RichContent gains plainText for the list's previews. Tests: the merge (chains, different and same sides, conflicts, resolving, three heads, missing parents, arrival order), the payload, the repository against a fake gateway, both blocs, and the widgets, including that cancelling a delete keeps the card. The sync tests, which need the running server, cover merging and conflicts across two devices and blank revisions on the server; their device helper is now shared in test/sync/support. AGENTS.md describes the revision pattern, the fractional order, and two tooling traps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 14:41:44 +02:00
);
}
/// A device: its own client and session, its own local database, and the
/// app's repositories over it. It syncs only when told to.
final class SyncDevice(
final protocol.Client _client,
final OfflineSyncDatabaseSession _db,
final SyncService _syncRequests,
/// The app's studysets on this device's database.
final StudysetRepository studysets,
/// The app's cards on this device's database.
final CardRepository cards,
feat(app): study with FSRS A studyset's card list gains "Study". The study screen shows the front, then the answer and the four ratings, each labelled with when the card would come back; every rating is recorded with how long answering took, and the queue is worked out anew after each. Schedules are never stored: scheduleOf replays a card's reviews in time order with FSRS-6 (fsrs v2.0.1, pinned exactly so every device on one app version computes the same). The package's fuzz can only be seeded through an API meant for tests, and due dates don't feed back into a card's state, so the replay runs without fuzz and fuzzedDays spreads the final due date over FSRS's range, picked by a stable FNV-1a fraction of the card's id and review count. studyQueue takes due cards earliest first, then new cards in studyset order, 20 per studyset and local day, and brings a learning card due within 20 minutes early when nothing else is due. Deleting a card deletes its reviews. Tests: the replay (learning, growing intervals, the same schedule whatever order reviews arrive in, previews matching what a rating yields), the fuzz range, the queue (order, the daily limit counted per local day, learn-ahead), the repository, the session bloc under a controlled clock, the screen, and a sync test for ratings on two devices. AGENTS.md records the review log and scheduling, and how plugin lint rules are ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 16:59:07 +02:00
feat: notes in studysets, synced and merged as cards are, with tags Server: three synced tables, only additions (migration 20261002191122122-notes): `note` (its studyset, a fractional position), `note_revision` (append-only, as a card's: parents, author, report, the payload {schemaVersion, title, body}) and `note_tag` (one row per assignment, never updated, as `card_tag`). Each indexes `spaceId`, and everything goes with its space, studyset, note or tag. A migration of synced tables locks app builds from before it out of syncing (ADR 0002, rule 3): cheap now, before the first release. App, `feature/note`: - `NoteContent` is a title and rich text, merged apart (ADR 0003): a title changed on one device and the body on another both survive, a body changed on two is a conflict. - `NoteRepository` creates a note at the end of its studyset, saves an edit on the revisions it started from (`saveEdit`, as the card composer now does), says when a note was last edited (its newest head's UUID v7), counts notes per studyset, and deletes a note's revisions blanked first (ADR 0002, rule 2). - Notes share the person's tags: `setTagsOfNote`, and folding tags of one name or undoing a tag's deletion covers notes too. - The delayed deletion with its Undo moves to `core/async` (`DelayedDeletions`): `StudysetDeletions` builds on it and now takes a studyset's notes too; `NoteDeletions` deletes a single note. Tests: content, merge, payload, repository, deletions, tags, the device migration guard, and sync tests against the server (four). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-02 21:53:56 +02:00
/// The app's notes on this device's database.
final NoteRepository notes,
feat(app): study with FSRS A studyset's card list gains "Study". The study screen shows the front, then the answer and the four ratings, each labelled with when the card would come back; every rating is recorded with how long answering took, and the queue is worked out anew after each. Schedules are never stored: scheduleOf replays a card's reviews in time order with FSRS-6 (fsrs v2.0.1, pinned exactly so every device on one app version computes the same). The package's fuzz can only be seeded through an API meant for tests, and due dates don't feed back into a card's state, so the replay runs without fuzz and fuzzedDays spreads the final due date over FSRS's range, picked by a stable FNV-1a fraction of the card's id and review count. studyQueue takes due cards earliest first, then new cards in studyset order, 20 per studyset and local day, and brings a learning card due within 20 minutes early when nothing else is due. Deleting a card deletes its reviews. Tests: the replay (learning, growing intervals, the same schedule whatever order reviews arrive in, previews matching what a rating yields), the fuzz range, the queue (order, the daily limit counted per local day, learn-ahead), the repository, the session bloc under a controlled clock, the screen, and a sync test for ratings on two devices. AGENTS.md records the review log and scheduling, and how plugin lint rules are ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 16:59:07 +02:00
/// The app's reviews on this device's database.
final ReviewRepository reviews,
/// The app's tags on this device's database.
final TagRepository tags,
/// The app's settings on this device's database.
final SettingsRepository settings,
Import Anki decks with their files and history Files in content: the person's images and sounds as asset rows synced like all content, their files kept by hash on each device, uploaded after each sync, downloaded on demand and checked against their hash (core/asset/AssetRepository). Rich text names them as asset:<id> URLs, in the editor's image block and an audio block of ours; any other URL is never fetched. Sounds play through audioplayers, one at a time. Content holding only an image or a sound isn't blank, and lists show it as such. The Anki import ("Import from Anki" under Add material): .apkg and .colpkg packages in all three formats Anki has written, read on an isolate of their own. Modern packages compress everything with zstd in frames that don't record their size, which pure_zstd v1.0.0 refuses; our fork decodes them. Fields become rich text by a converter of our own (lines, lists, formatting, images, sounds, cloze deletions hidden on the front). One card per note, the files first, studysets as chosen (flatten, sub-studysets, or tags by subdeck), the notes' tags, and, if wanted, the review history, which FSRS replays: the first template's reviews to the card's schedule, a reversed note type's second to the reverse one. Imported cards say where they came from in their first revision (card schema 3); the dashboard doesn't count them as written. The test fixtures are exported by Anki itself (tool/anki/make_fixtures.py). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-02 04:52:02 +02:00
/// The app's files on this device's database and cache; uploads only when
/// told to.
final AssetRepository assets,
feat(app): flashcards on append-only revisions A studyset's screen lists its cards (reorderable; a marker on cards changed on two devices) and opens a card's screen with an editor for each side. Saving appends a revision whose parents are all current heads. mergeRevisions merges several heads per side against their newest common ancestor, as decided: different sides merge automatically, the same side changed differently is a conflict. The card screen then shows every version of that side with "Keep this", and saving is possible once each conflicted side is settled, which resolves the conflict for every device. Nothing is merged silently inside a side. CardRepository watches a studyset's cards, creates at the end and moves by fractional index (fractional_indexing_dart, checked against the reference algorithm's test vectors), and deletes by blanking every revision first (ADR 0002, rule 2). Deleting a studyset now deletes its cards the same way before the studyset goes. Every person gets a CardRepository whose revisions name them as author. RichContent gains plainText for the list's previews. Tests: the merge (chains, different and same sides, conflicts, resolving, three heads, missing parents, arrival order), the payload, the repository against a fake gateway, both blocs, and the widgets, including that cancelling a delete keeps the card. The sync tests, which need the running server, cover merging and conflicts across two devices and blank revisions on the server; their device helper is now shared in test/sync/support. AGENTS.md describes the revision pattern, the fractional order, and two tooling traps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 14:41:44 +02:00
) {
/// Opens a device signed in as [person], whose local database in
/// [database] claims [space] as its personal space.
static Future<SyncDevice> open(
AuthSuccess person,
File database, {
required protocol.UuidValue space,
}) async {
final session = ClientAuthSessionManager(storage: _MemoryAuthStorage());
final client = protocol.Client(_serverUrl)..authSessionManager = session;
await session.updateSignedInUser(person);
final db = await client.createSyncSession(
database.path,
persistentUserId: space,
);
// The gateways ask for a sync after every write; these tests sync
// explicitly instead, so the requests go nowhere.
final syncRequests = SyncService(({required onMerge}) async {});
final cards = CardRepository(
SyncCardGateway(db, syncRequests, person.authUserId.uuid),
);
feat: notes in studysets, synced and merged as cards are, with tags Server: three synced tables, only additions (migration 20261002191122122-notes): `note` (its studyset, a fractional position), `note_revision` (append-only, as a card's: parents, author, report, the payload {schemaVersion, title, body}) and `note_tag` (one row per assignment, never updated, as `card_tag`). Each indexes `spaceId`, and everything goes with its space, studyset, note or tag. A migration of synced tables locks app builds from before it out of syncing (ADR 0002, rule 3): cheap now, before the first release. App, `feature/note`: - `NoteContent` is a title and rich text, merged apart (ADR 0003): a title changed on one device and the body on another both survive, a body changed on two is a conflict. - `NoteRepository` creates a note at the end of its studyset, saves an edit on the revisions it started from (`saveEdit`, as the card composer now does), says when a note was last edited (its newest head's UUID v7), counts notes per studyset, and deletes a note's revisions blanked first (ADR 0002, rule 2). - Notes share the person's tags: `setTagsOfNote`, and folding tags of one name or undoing a tag's deletion covers notes too. - The delayed deletion with its Undo moves to `core/async` (`DelayedDeletions`): `StudysetDeletions` builds on it and now takes a studyset's notes too; `NoteDeletions` deletes a single note. Tests: content, merge, payload, repository, deletions, tags, the device migration guard, and sync tests against the server (four). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-02 21:53:56 +02:00
final notes = NoteRepository(
SyncNoteGateway(db, syncRequests, person.authUserId.uuid),
);
feat(app): flashcards on append-only revisions A studyset's screen lists its cards (reorderable; a marker on cards changed on two devices) and opens a card's screen with an editor for each side. Saving appends a revision whose parents are all current heads. mergeRevisions merges several heads per side against their newest common ancestor, as decided: different sides merge automatically, the same side changed differently is a conflict. The card screen then shows every version of that side with "Keep this", and saving is possible once each conflicted side is settled, which resolves the conflict for every device. Nothing is merged silently inside a side. CardRepository watches a studyset's cards, creates at the end and moves by fractional index (fractional_indexing_dart, checked against the reference algorithm's test vectors), and deletes by blanking every revision first (ADR 0002, rule 2). Deleting a studyset now deletes its cards the same way before the studyset goes. Every person gets a CardRepository whose revisions name them as author. RichContent gains plainText for the list's previews. Tests: the merge (chains, different and same sides, conflicts, resolving, three heads, missing parents, arrival order), the payload, the repository against a fake gateway, both blocs, and the widgets, including that cancelling a delete keeps the card. The sync tests, which need the running server, cover merging and conflicts across two devices and blank revisions on the server; their device helper is now shared in test/sync/support. AGENTS.md describes the revision pattern, the fractional order, and two tooling traps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 14:41:44 +02:00
return SyncDevice(
client,
db,
syncRequests,
StudysetRepository(SyncStudysetGateway(db, syncRequests)),
cards,
feat: notes in studysets, synced and merged as cards are, with tags Server: three synced tables, only additions (migration 20261002191122122-notes): `note` (its studyset, a fractional position), `note_revision` (append-only, as a card's: parents, author, report, the payload {schemaVersion, title, body}) and `note_tag` (one row per assignment, never updated, as `card_tag`). Each indexes `spaceId`, and everything goes with its space, studyset, note or tag. A migration of synced tables locks app builds from before it out of syncing (ADR 0002, rule 3): cheap now, before the first release. App, `feature/note`: - `NoteContent` is a title and rich text, merged apart (ADR 0003): a title changed on one device and the body on another both survive, a body changed on two is a conflict. - `NoteRepository` creates a note at the end of its studyset, saves an edit on the revisions it started from (`saveEdit`, as the card composer now does), says when a note was last edited (its newest head's UUID v7), counts notes per studyset, and deletes a note's revisions blanked first (ADR 0002, rule 2). - Notes share the person's tags: `setTagsOfNote`, and folding tags of one name or undoing a tag's deletion covers notes too. - The delayed deletion with its Undo moves to `core/async` (`DelayedDeletions`): `StudysetDeletions` builds on it and now takes a studyset's notes too; `NoteDeletions` deletes a single note. Tests: content, merge, payload, repository, deletions, tags, the device migration guard, and sync tests against the server (four). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-02 21:53:56 +02:00
notes,
feat(app): study with FSRS A studyset's card list gains "Study". The study screen shows the front, then the answer and the four ratings, each labelled with when the card would come back; every rating is recorded with how long answering took, and the queue is worked out anew after each. Schedules are never stored: scheduleOf replays a card's reviews in time order with FSRS-6 (fsrs v2.0.1, pinned exactly so every device on one app version computes the same). The package's fuzz can only be seeded through an API meant for tests, and due dates don't feed back into a card's state, so the replay runs without fuzz and fuzzedDays spreads the final due date over FSRS's range, picked by a stable FNV-1a fraction of the card's id and review count. studyQueue takes due cards earliest first, then new cards in studyset order, 20 per studyset and local day, and brings a learning card due within 20 minutes early when nothing else is due. Deleting a card deletes its reviews. Tests: the replay (learning, growing intervals, the same schedule whatever order reviews arrive in, previews matching what a rating yields), the fuzz range, the queue (order, the daily limit counted per local day, learn-ahead), the repository, the session bloc under a controlled clock, the screen, and a sync test for ratings on two devices. AGENTS.md records the review log and scheduling, and how plugin lint rules are ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 16:59:07 +02:00
ReviewRepository(SyncReviewGateway(db, syncRequests)),
TagRepository(
SyncTagGateway(db, syncRequests),
cardChanges: cards.changes,
feat: notes in studysets, synced and merged as cards are, with tags Server: three synced tables, only additions (migration 20261002191122122-notes): `note` (its studyset, a fractional position), `note_revision` (append-only, as a card's: parents, author, report, the payload {schemaVersion, title, body}) and `note_tag` (one row per assignment, never updated, as `card_tag`). Each indexes `spaceId`, and everything goes with its space, studyset, note or tag. A migration of synced tables locks app builds from before it out of syncing (ADR 0002, rule 3): cheap now, before the first release. App, `feature/note`: - `NoteContent` is a title and rich text, merged apart (ADR 0003): a title changed on one device and the body on another both survive, a body changed on two is a conflict. - `NoteRepository` creates a note at the end of its studyset, saves an edit on the revisions it started from (`saveEdit`, as the card composer now does), says when a note was last edited (its newest head's UUID v7), counts notes per studyset, and deletes a note's revisions blanked first (ADR 0002, rule 2). - Notes share the person's tags: `setTagsOfNote`, and folding tags of one name or undoing a tag's deletion covers notes too. - The delayed deletion with its Undo moves to `core/async` (`DelayedDeletions`): `StudysetDeletions` builds on it and now takes a studyset's notes too; `NoteDeletions` deletes a single note. Tests: content, merge, payload, repository, deletions, tags, the device migration guard, and sync tests against the server (four). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-02 21:53:56 +02:00
noteChanges: notes.changes,
),
SettingsRepository(SyncSettingsGateway(db, syncRequests)),
Import Anki decks with their files and history Files in content: the person's images and sounds as asset rows synced like all content, their files kept by hash on each device, uploaded after each sync, downloaded on demand and checked against their hash (core/asset/AssetRepository). Rich text names them as asset:<id> URLs, in the editor's image block and an audio block of ours; any other URL is never fetched. Sounds play through audioplayers, one at a time. Content holding only an image or a sound isn't blank, and lists show it as such. The Anki import ("Import from Anki" under Add material): .apkg and .colpkg packages in all three formats Anki has written, read on an isolate of their own. Modern packages compress everything with zstd in frames that don't record their size, which pure_zstd v1.0.0 refuses; our fork decodes them. Fields become rich text by a converter of our own (lines, lists, formatting, images, sounds, cloze deletions hidden on the front). One card per note, the files first, studysets as chosen (flatten, sub-studysets, or tags by subdeck), the notes' tags, and, if wanted, the review history, which FSRS replays: the first template's reviews to the card's schedule, a reversed note type's second to the reverse one. Imported cards say where they came from in their first revision (card schema 3); the dashboard doesn't count them as written. The test fixtures are exported by Anki itself (tool/anki/make_fixtures.py). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-02 04:52:02 +02:00
AssetRepository(
SyncAssetGateway(db, syncRequests),
ServerpodAssetServerGateway(client),
DirectoryAssetFileCache(Directory('${database.path}.files')),
),
feat(app): flashcards on append-only revisions A studyset's screen lists its cards (reorderable; a marker on cards changed on two devices) and opens a card's screen with an editor for each side. Saving appends a revision whose parents are all current heads. mergeRevisions merges several heads per side against their newest common ancestor, as decided: different sides merge automatically, the same side changed differently is a conflict. The card screen then shows every version of that side with "Keep this", and saving is possible once each conflicted side is settled, which resolves the conflict for every device. Nothing is merged silently inside a side. CardRepository watches a studyset's cards, creates at the end and moves by fractional index (fractional_indexing_dart, checked against the reference algorithm's test vectors), and deletes by blanking every revision first (ADR 0002, rule 2). Deleting a studyset now deletes its cards the same way before the studyset goes. Every person gets a CardRepository whose revisions name them as author. RichContent gains plainText for the list's previews. Tests: the merge (chains, different and same sides, conflicts, resolving, three heads, missing parents, arrival order), the payload, the repository against a fake gateway, both blocs, and the widgets, including that cancelling a delete keeps the card. The sync tests, which need the running server, cover merging and conflicts across two devices and blank revisions on the server; their device helper is now shared in test/sync/support. AGENTS.md describes the revision pattern, the fractional order, and two tooling traps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 14:41:44 +02:00
);
}
/// Sends this device's changes and receives everyone else's.
Future<void> sync() => _client.offlineSync.syncOnce(_db);
/// The studyset with [id] on this device, or null.
Future<Studyset?> studyset(String id) async {
for (final studyset in await studysets.all()) {
if (studyset.id == id) {
return studyset;
}
}
return null;
}
/// The card with [id] in [studysetId] on this device, or null.
Future<Card?> card(String studysetId, String id) async {
for (final card in await cards.cardsOf(studysetId)) {
if (card.id == id) {
return card;
}
}
return null;
}
feat: notes in studysets, synced and merged as cards are, with tags Server: three synced tables, only additions (migration 20261002191122122-notes): `note` (its studyset, a fractional position), `note_revision` (append-only, as a card's: parents, author, report, the payload {schemaVersion, title, body}) and `note_tag` (one row per assignment, never updated, as `card_tag`). Each indexes `spaceId`, and everything goes with its space, studyset, note or tag. A migration of synced tables locks app builds from before it out of syncing (ADR 0002, rule 3): cheap now, before the first release. App, `feature/note`: - `NoteContent` is a title and rich text, merged apart (ADR 0003): a title changed on one device and the body on another both survive, a body changed on two is a conflict. - `NoteRepository` creates a note at the end of its studyset, saves an edit on the revisions it started from (`saveEdit`, as the card composer now does), says when a note was last edited (its newest head's UUID v7), counts notes per studyset, and deletes a note's revisions blanked first (ADR 0002, rule 2). - Notes share the person's tags: `setTagsOfNote`, and folding tags of one name or undoing a tag's deletion covers notes too. - The delayed deletion with its Undo moves to `core/async` (`DelayedDeletions`): `StudysetDeletions` builds on it and now takes a studyset's notes too; `NoteDeletions` deletes a single note. Tests: content, merge, payload, repository, deletions, tags, the device migration guard, and sync tests against the server (four). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-02 21:53:56 +02:00
/// The note with [id] in [studysetId] on this device, or null.
Future<Note?> note(String studysetId, String id) async {
for (final note in await notes.notesOf(studysetId)) {
if (note.id == id) {
return note;
}
}
return null;
}
feat(app): flashcards on append-only revisions A studyset's screen lists its cards (reorderable; a marker on cards changed on two devices) and opens a card's screen with an editor for each side. Saving appends a revision whose parents are all current heads. mergeRevisions merges several heads per side against their newest common ancestor, as decided: different sides merge automatically, the same side changed differently is a conflict. The card screen then shows every version of that side with "Keep this", and saving is possible once each conflicted side is settled, which resolves the conflict for every device. Nothing is merged silently inside a side. CardRepository watches a studyset's cards, creates at the end and moves by fractional index (fractional_indexing_dart, checked against the reference algorithm's test vectors), and deletes by blanking every revision first (ADR 0002, rule 2). Deleting a studyset now deletes its cards the same way before the studyset goes. Every person gets a CardRepository whose revisions name them as author. RichContent gains plainText for the list's previews. Tests: the merge (chains, different and same sides, conflicts, resolving, three heads, missing parents, arrival order), the payload, the repository against a fake gateway, both blocs, and the widgets, including that cancelling a delete keeps the card. The sync tests, which need the running server, cover merging and conflicts across two devices and blank revisions on the server; their device helper is now shared in test/sync/support. AGENTS.md describes the revision pattern, the fractional order, and two tooling traps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 14:41:44 +02:00
/// Closes everything the device opened.
Future<void> close() async {
await studysets.dispose();
await tags.dispose();
await settings.dispose();
await reviews.dispose();
feat(app): flashcards on append-only revisions A studyset's screen lists its cards (reorderable; a marker on cards changed on two devices) and opens a card's screen with an editor for each side. Saving appends a revision whose parents are all current heads. mergeRevisions merges several heads per side against their newest common ancestor, as decided: different sides merge automatically, the same side changed differently is a conflict. The card screen then shows every version of that side with "Keep this", and saving is possible once each conflicted side is settled, which resolves the conflict for every device. Nothing is merged silently inside a side. CardRepository watches a studyset's cards, creates at the end and moves by fractional index (fractional_indexing_dart, checked against the reference algorithm's test vectors), and deletes by blanking every revision first (ADR 0002, rule 2). Deleting a studyset now deletes its cards the same way before the studyset goes. Every person gets a CardRepository whose revisions name them as author. RichContent gains plainText for the list's previews. Tests: the merge (chains, different and same sides, conflicts, resolving, three heads, missing parents, arrival order), the payload, the repository against a fake gateway, both blocs, and the widgets, including that cancelling a delete keeps the card. The sync tests, which need the running server, cover merging and conflicts across two devices and blank revisions on the server; their device helper is now shared in test/sync/support. AGENTS.md describes the revision pattern, the fractional order, and two tooling traps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 14:41:44 +02:00
await cards.dispose();
feat: notes in studysets, synced and merged as cards are, with tags Server: three synced tables, only additions (migration 20261002191122122-notes): `note` (its studyset, a fractional position), `note_revision` (append-only, as a card's: parents, author, report, the payload {schemaVersion, title, body}) and `note_tag` (one row per assignment, never updated, as `card_tag`). Each indexes `spaceId`, and everything goes with its space, studyset, note or tag. A migration of synced tables locks app builds from before it out of syncing (ADR 0002, rule 3): cheap now, before the first release. App, `feature/note`: - `NoteContent` is a title and rich text, merged apart (ADR 0003): a title changed on one device and the body on another both survive, a body changed on two is a conflict. - `NoteRepository` creates a note at the end of its studyset, saves an edit on the revisions it started from (`saveEdit`, as the card composer now does), says when a note was last edited (its newest head's UUID v7), counts notes per studyset, and deletes a note's revisions blanked first (ADR 0002, rule 2). - Notes share the person's tags: `setTagsOfNote`, and folding tags of one name or undoing a tag's deletion covers notes too. - The delayed deletion with its Undo moves to `core/async` (`DelayedDeletions`): `StudysetDeletions` builds on it and now takes a studyset's notes too; `NoteDeletions` deletes a single note. Tests: content, merge, payload, repository, deletions, tags, the device migration guard, and sync tests against the server (four). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-02 21:53:56 +02:00
await notes.dispose();
Import Anki decks with their files and history Files in content: the person's images and sounds as asset rows synced like all content, their files kept by hash on each device, uploaded after each sync, downloaded on demand and checked against their hash (core/asset/AssetRepository). Rich text names them as asset:<id> URLs, in the editor's image block and an audio block of ours; any other URL is never fetched. Sounds play through audioplayers, one at a time. Content holding only an image or a sound isn't blank, and lists show it as such. The Anki import ("Import from Anki" under Add material): .apkg and .colpkg packages in all three formats Anki has written, read on an isolate of their own. Modern packages compress everything with zstd in frames that don't record their size, which pure_zstd v1.0.0 refuses; our fork decodes them. Fields become rich text by a converter of our own (lines, lists, formatting, images, sounds, cloze deletions hidden on the front). One card per note, the files first, studysets as chosen (flatten, sub-studysets, or tags by subdeck), the notes' tags, and, if wanted, the review history, which FSRS replays: the first template's reviews to the card's schedule, a reversed note type's second to the reverse one. Imported cards say where they came from in their first revision (card schema 3); the dashboard doesn't count them as written. The test fixtures are exported by Anki itself (tool/anki/make_fixtures.py). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-02 04:52:02 +02:00
await assets.dispose();
feat(app): flashcards on append-only revisions A studyset's screen lists its cards (reorderable; a marker on cards changed on two devices) and opens a card's screen with an editor for each side. Saving appends a revision whose parents are all current heads. mergeRevisions merges several heads per side against their newest common ancestor, as decided: different sides merge automatically, the same side changed differently is a conflict. The card screen then shows every version of that side with "Keep this", and saving is possible once each conflicted side is settled, which resolves the conflict for every device. Nothing is merged silently inside a side. CardRepository watches a studyset's cards, creates at the end and moves by fractional index (fractional_indexing_dart, checked against the reference algorithm's test vectors), and deletes by blanking every revision first (ADR 0002, rule 2). Deleting a studyset now deletes its cards the same way before the studyset goes. Every person gets a CardRepository whose revisions name them as author. RichContent gains plainText for the list's previews. Tests: the merge (chains, different and same sides, conflicts, resolving, three heads, missing parents, arrival order), the payload, the repository against a fake gateway, both blocs, and the widgets, including that cancelling a delete keeps the card. The sync tests, which need the running server, cover merging and conflicts across two devices and blank revisions on the server; their device helper is now shared in test/sync/support. AGENTS.md describes the revision pattern, the fractional order, and two tooling traps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 14:41:44 +02:00
await _syncRequests.dispose();
await _db.close();
_client.close();
}
}
/// Keeps a device's session in memory, where the app uses secure storage.
final class _MemoryAuthStorage() implements ClientAuthSuccessStorage {
AuthSuccess? _session;
@override
Future<AuthSuccess?> get() async => _session;
@override
Future<void> set(AuthSuccess? data) async {
_session = data;
}
}