import 'dart:async'; import 'dart:typed_data'; import 'package:clock/clock.dart'; import 'package:halkyon_learn_flutter/src/core/content/payload.dart'; import 'package:halkyon_learn_flutter/src/feature/note/data/note_gateway.dart'; /// A [NoteGateway] in memory. Ids sort by creation, as UUID v7 ids do: notes /// are `note-0001`, `note-0002`, …, revisions `nrev-0001`, …, each written /// at the clock's time; [receiveRemoteRevision] plays a sync that brought /// another device's edit. class FakeNoteGateway() implements NoteGateway { final _notes = {}; final _revisions = {}; final _remoteChanges = StreamController.broadcast(); var _nextId = 1; /// Every revision written on this device, in order. final written = []; /// The deletion steps, in order: `blank `, `delete `. final deletions = []; /// When set, storing a note or a revision fails with it. Exception? failWrites; /// The payload each revision holds now, by revision id. Map 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 noteId, required Set parents, required ByteData payload, }) { _revisions[id] = ( id: id, noteId: noteId, parents: parents, payload: payload, writtenAt: clock.now().toUtc(), ); _remoteChanges.add(null); } /// Deletes the note [noteId] as if another device had done it and a sync /// had brought it here. void receiveRemoteDeletion(String noteId) { _notes.remove(noteId); _remoteChanges.add(null); } @override Future> notesOf(String studysetId) async => [ for (final MapEntry(key: id, value: note) in _notes.entries) if (note.studysetId == studysetId) (id: id, position: note.position), ]; @override Future> allNotes() async => [ for (final MapEntry(key: id, value: note) in _notes.entries) (id: id, studysetId: note.studysetId), ]; @override Future> revisionsOf(Set noteIds) async => [ for (final revision in _revisions.values) if (noteIds.contains(revision.noteId)) revision, ]; @override Future<({String noteId, String revisionId})> insertNote({ required String studysetId, required String position, required ByteData payload, }) async { if (failWrites case final error?) { throw error; } final id = _newId('note'); _notes[id] = (studysetId: studysetId, position: position); final revisionId = await insertRevision( noteId: id, parents: const {}, payload: payload, ); return (noteId: id, revisionId: revisionId); } @override Future insertRevision({ required String noteId, required Set parents, required ByteData payload, }) async { if (failWrites case final error?) { throw error; } final revision = ( id: _newId('nrev'), noteId: noteId, parents: parents, payload: payload, writtenAt: clock.now().toUtc(), ); _revisions[revision.id] = revision; written.add(revision); return revision.id; } @override Future updatePosition(String noteId, String position) async { if (_notes[noteId] case final note?) { _notes[noteId] = (studysetId: note.studysetId, position: position); } } @override Future blankRevisions(String noteId) async { deletions.add('blank $noteId'); for (final revision in [..._revisions.values]) { if (revision.noteId == noteId) { _revisions[revision.id] = ( id: revision.id, noteId: noteId, parents: revision.parents, payload: redactedPayload, writtenAt: revision.writtenAt, ); } } } @override Future deleteNote(String noteId) async { deletions.add('delete $noteId'); _notes.remove(noteId); } @override Stream get remoteChanges => _remoteChanges.stream; String _newId(String kind) => '$kind-${(_nextId++).toString().padLeft(4, '0')}'; }