halkyon/halkyon_learn_flutter/test/support/fake_studyset_gateway.dart
Tizian 「ティツィアーン」 4644125cc4
feat(studyset): deleted studysets wait 14 days in Recently deleted
Deleting a studyset blanked it with everything inside it once a
four-second Undo was over. It now only marks it (`studyset.deletedAt`, a
column as `archivedAt`): it leaves every list, count and session with
what is inside it and waits in the library's Recently deleted for 14
days, saying how many are left, with Restore and Delete now. Deleting no
longer asks first and offers Undo; Delete now asks. Once the 14 days are
over, whichever of the person's devices looks next (as their data opens,
after every sync) deletes it for good, blanking first as before; the
server can't blank a payload and never writes a person's space (ADR
0007). The connector, publishing and the learner count leave deleted
studysets out too.

The migration adds one nullable column: app versions before it stop
syncing until they update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:43 +02:00

220 lines
5.1 KiB
Dart

import 'dart:async';
import 'dart:typed_data';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_gateway.dart';
/// A [StudysetGateway] in memory: ids are `studyset-1`, `studyset-2`, …, and
/// [receiveRemoteChange] and [receiveRemoteDeletion] play a sync that brought
/// changes.
class FakeStudysetGateway() implements StudysetGateway {
final _rows = <String, StudysetRow>{};
final _remoteChanges = StreamController<void>.broadcast();
var _nextId = 1;
/// Every payload written, in order, as (id, payload).
final writes = <(String, ByteData)>[];
/// The ids of the deleted rows, in order.
final deleted = <String>[];
/// Every move, in order, as (id, parent, position).
final moves = <(String, String?, String)>[];
/// When set, reading the studysets fails with it.
Exception? failReads;
/// Stores [payload] as if another device had written it and a sync had
/// brought it here, with the other columns given.
void receiveRemoteChange(
String id,
ByteData payload, {
String? parentId,
String position = 'a0',
DateTime? archivedAt,
DateTime? deletedAt,
DateTime? starredAt,
DateTime? updatedAt,
}) {
_rows[id] = (
id: id,
payload: payload,
parentId: parentId,
position: position,
archivedAt: archivedAt,
deletedAt: deletedAt,
starredAt: starredAt,
updatedAt: updatedAt,
);
_remoteChanges.add(null);
}
/// Removes the studyset [id] as if another device had deleted it and a
/// sync had brought that here.
void receiveRemoteDeletion(String id) {
_rows.remove(id);
_remoteChanges.add(null);
}
@override
Future<List<StudysetRow>> all() async {
if (failReads case final error?) {
throw error;
}
return [..._rows.values];
}
@override
Future<String> insert(
ByteData payload,
DateTime updatedAt, {
String? parentId,
String position = 'a0',
}) async {
final id = 'studyset-${_nextId++}';
_rows[id] = (
id: id,
payload: payload,
parentId: parentId,
position: position,
archivedAt: null,
deletedAt: null,
starredAt: null,
updatedAt: updatedAt,
);
writes.add((id, payload));
return id;
}
@override
Future<void> updatePayload(
String id,
ByteData payload,
DateTime updatedAt,
) async {
if (_rows[id] case final row?) {
_rows[id] = (
id: id,
payload: payload,
parentId: row.parentId,
position: row.position,
archivedAt: row.archivedAt,
deletedAt: row.deletedAt,
starredAt: row.starredAt,
updatedAt: updatedAt,
);
writes.add((id, payload));
}
}
@override
Future<void> updateStarredAt(
String id,
DateTime? starredAt,
DateTime updatedAt,
) async {
if (_rows[id] case final row?) {
_rows[id] = (
id: id,
payload: row.payload,
parentId: row.parentId,
position: row.position,
archivedAt: row.archivedAt,
deletedAt: row.deletedAt,
starredAt: starredAt,
updatedAt: updatedAt,
);
}
}
@override
Future<void> updateParent(
String id,
String? parentId,
String position,
DateTime updatedAt,
) async {
if (_rows[id] case final row?) {
_rows[id] = (
id: id,
payload: row.payload,
parentId: parentId,
position: position,
archivedAt: row.archivedAt,
deletedAt: row.deletedAt,
starredAt: row.starredAt,
updatedAt: updatedAt,
);
moves.add((id, parentId, position));
}
}
@override
Future<void> updatePosition(
String id,
String position,
DateTime updatedAt,
) async {
if (_rows[id] case final row?) {
_rows[id] = (
id: id,
payload: row.payload,
parentId: row.parentId,
position: position,
archivedAt: row.archivedAt,
deletedAt: row.deletedAt,
starredAt: row.starredAt,
updatedAt: updatedAt,
);
moves.add((id, row.parentId, position));
}
}
@override
Future<void> updateArchivedAt(
String id,
DateTime? archivedAt,
DateTime updatedAt,
) async {
if (_rows[id] case final row?) {
_rows[id] = (
id: id,
payload: row.payload,
parentId: row.parentId,
position: row.position,
archivedAt: archivedAt,
deletedAt: row.deletedAt,
starredAt: row.starredAt,
updatedAt: updatedAt,
);
}
}
@override
Future<void> updateDeletedAt(
String id,
DateTime? deletedAt,
DateTime updatedAt,
) async {
if (_rows[id] case final row?) {
_rows[id] = (
id: id,
payload: row.payload,
parentId: row.parentId,
position: row.position,
archivedAt: row.archivedAt,
deletedAt: deletedAt,
starredAt: row.starredAt,
updatedAt: updatedAt,
);
}
}
@override
Future<void> delete(String id) async {
_rows.remove(id);
deleted.add(id);
}
@override
Stream<void> get remoteChanges => _remoteChanges.stream;
}