halkyon/halkyon_learn_flutter/test/support/fake_card_gateway.dart
Tizian 「ティツィアーン」 a5515c13f7
feat(app): the card composer's state: many cards, saved on leaving
A bloc holding a studyset's cards as drafts, new ones on top and the
stored ones below. A card is saved when the person leaves it, once it
changed and has everything its type asks for, only its type's fields
stored; a failed save waits for the next leaving. A new card takes the
type of the card before it and what is pinned of it (front, answer,
tags); duplicating, clearing and deleting; a card devices changed
differently first needs a version chosen for every field in conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-01 21:10:02 +02:00

126 lines
3.6 KiB
Dart

import 'dart:async';
import 'dart:typed_data';
import 'package:halkyon_learn_flutter/src/core/content/payload.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_gateway.dart';
/// A [CardGateway] in memory. Ids sort by creation, as UUID v7 ids do: cards
/// are `card-0001`, `card-0002`, …, revisions `rev-0001`, …;
/// [receiveRemoteRevision] plays a sync that brought another device's edit.
class FakeCardGateway() implements CardGateway {
final _cards = <String, ({String studysetId, String position})>{};
final _revisions = <String, CardRevisionRow>{};
final _remoteChanges = StreamController<void>.broadcast();
var _nextId = 1;
/// Every revision written on this device, in order.
final written = <CardRevisionRow>[];
/// The deletion steps, in order: `blank <card id>`, `delete <card id>`.
final deletions = <String>[];
/// When set, storing a card or a revision fails with it.
Exception? failWrites;
/// The payload each revision holds now, by revision id.
Map<String, ByteData> get payloads => {
for (final revision in _revisions.values) revision.id: revision.payload,
};
/// Stores a revision as if another device had written it and a sync had
/// brought it here.
void receiveRemoteRevision({
required String id,
required String cardId,
required Set<String> parents,
required ByteData payload,
}) {
_revisions[id] = (
id: id,
cardId: cardId,
parents: parents,
payload: payload,
);
_remoteChanges.add(null);
}
@override
Future<List<CardRow>> cardsOf(String studysetId) async => [
for (final MapEntry(key: id, value: card) in _cards.entries)
if (card.studysetId == studysetId) (id: id, position: card.position),
];
@override
Future<List<CardRevisionRow>> revisionsOf(Set<String> cardIds) async => [
for (final revision in _revisions.values)
if (cardIds.contains(revision.cardId)) revision,
];
@override
Future<String> insertCard({
required String studysetId,
required String position,
required ByteData payload,
}) async {
if (failWrites case final error?) {
throw error;
}
final id = _newId('card');
_cards[id] = (studysetId: studysetId, position: position);
await insertRevision(cardId: id, parents: const {}, payload: payload);
return id;
}
@override
Future<void> insertRevision({
required String cardId,
required Set<String> parents,
required ByteData payload,
}) async {
if (failWrites case final error?) {
throw error;
}
final revision = (
id: _newId('rev'),
cardId: cardId,
parents: parents,
payload: payload,
);
_revisions[revision.id] = revision;
written.add(revision);
}
@override
Future<void> updatePosition(String cardId, String position) async {
if (_cards[cardId] case final card?) {
_cards[cardId] = (studysetId: card.studysetId, position: position);
}
}
@override
Future<void> blankRevisions(String cardId) async {
deletions.add('blank $cardId');
for (final revision in [..._revisions.values]) {
if (revision.cardId == cardId) {
_revisions[revision.id] = (
id: revision.id,
cardId: cardId,
parents: revision.parents,
payload: redactedPayload,
);
}
}
}
@override
Future<void> deleteCard(String cardId) async {
deletions.add('delete $cardId');
_cards.remove(cardId);
}
@override
Stream<void> get remoteChanges => _remoteChanges.stream;
String _newId(String kind) =>
'$kind-${(_nextId++).toString().padLeft(4, '0')}';
}