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>
126 lines
3.6 KiB
Dart
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')}';
|
|
}
|