refactor(content,core,app,editor,learn): Learn's content in pure Dart, for its agent
ADR 0044's D49 (amended in building it): what reads and seals Learn's content moves out of the Flutter packages, so Learn's agent, which runs without Flutter, shares it rather than repeating the privacy rules. packages/halkyon_content (new, pure Dart) takes from halkyon_app the envelope and its cipher, SealedFile, UnlockedAccountKeys, shared spaces' gateway with SharedKeys and KeyPins, FileCache and FileServerGateway, with their fakes and tests; halkyon_app exports it, so no app changes. Its rich_text.dart takes RichContent from halkyon_editor, which exports it. newAccountKeys makes an agent's key pair. halkyon_core takes DelayedDeletions and gains SyncRequests, what a synced gateway asks of the sync, which SyncService implements. apps/learn/halkyon_learn_core (new, pure Dart) takes Learn's privacy, sync, payloads and the models, codecs, gateways and repositories of studysets, cards, notes, tags and feedback, with what they reach of files, learning paths, citations and study options; the app imports them from there. A subject's icon, colour and name stay in the app (SubjectLook); what only the app shows (progress, exam coverage, the card browser, deletions' waits) stays too. Nothing changes for people using the apps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
82c777b1a5
commit
d09625d43e
3 changed files with 3517 additions and 2007 deletions
|
|
@ -16,7 +16,9 @@ library;
|
|||
export 'package:clock/clock.dart' show Clock, clock;
|
||||
|
||||
export 'src/async/coalescing_runner.dart';
|
||||
export 'src/async/delayed_deletions.dart';
|
||||
export 'src/async/serial_queue.dart';
|
||||
export 'src/async/sync_requests.dart';
|
||||
export 'src/bip39_english.dart';
|
||||
export 'src/exceptions.dart';
|
||||
export 'src/logger.dart';
|
||||
|
|
|
|||
105
packages/halkyon_core/lib/src/async/delayed_deletions.dart
Normal file
105
packages/halkyon_core/lib/src/async/delayed_deletions.dart
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
/// Deletions that can be undone for a moment (decided 2026-10-01): what is
|
||||
/// deleted leaves every list at once ([pending]), and only once the time to
|
||||
/// undo is over (for a deletion held, counted from when [delete] lets it go)
|
||||
/// does [_deleteNow] run, since blanking can't be undone (ADR 0002, rule 2).
|
||||
///
|
||||
/// It belongs to the person's data, so a deletion outlives the screen it
|
||||
/// started on, and it closes with the person's data: a deletion still
|
||||
/// waiting then is dropped, and what it would have deleted stays.
|
||||
class DelayedDeletions(
|
||||
/// Deletes an item, given its id, for good.
|
||||
final Future<void> Function(String id) _deleteNow, {
|
||||
|
||||
/// Names the log a failed deletion goes to.
|
||||
final String _logName = 'Deletions',
|
||||
|
||||
/// How long a deletion can be undone: the three seconds the message
|
||||
/// offering it shows, and a moment for a late tap.
|
||||
final Duration _undoWindow = const Duration(seconds: 4),
|
||||
}) {
|
||||
/// The deletions that can still be undone, by the item's id: with the
|
||||
/// timer that starts it, or none while it is held.
|
||||
final _waiting = <String, Timer?>{};
|
||||
|
||||
/// The deletions under way, too late to undo.
|
||||
final _deleting = <String, Future<void>>{};
|
||||
|
||||
final _changes = StreamController<Set<String>>.broadcast();
|
||||
|
||||
/// The items whose deletion waits or is under way: lists leave them out.
|
||||
Set<String> get pending => {..._waiting.keys, ..._deleting.keys};
|
||||
|
||||
/// [pending], now and after every change.
|
||||
Stream<Set<String>> watchPending() => Stream.multi((listener) {
|
||||
listener.add(pending);
|
||||
final subscription = _changes.stream.listen(listener.add);
|
||||
listener.onCancel = subscription.cancel;
|
||||
});
|
||||
|
||||
/// Deletes the item [id] once the time to undo that is over.
|
||||
void delete(String id) {
|
||||
if (_deleting.containsKey(id)) {
|
||||
return;
|
||||
}
|
||||
_waiting[id]?.cancel();
|
||||
_waiting[id] = Timer(_undoWindow, () => _start(id));
|
||||
_changed();
|
||||
}
|
||||
|
||||
/// Takes the item [id] out of every list as [delete] does, but deletes it
|
||||
/// only once [delete] is called for it, however long that takes: e.g. at
|
||||
/// the end of a study session, which can undo it until then. Closing drops
|
||||
/// it as any deletion still waiting, and the item stays.
|
||||
void hold(String id) {
|
||||
if (_deleting.containsKey(id)) {
|
||||
return;
|
||||
}
|
||||
_waiting[id]?.cancel();
|
||||
_waiting[id] = null;
|
||||
_changed();
|
||||
}
|
||||
|
||||
/// Keeps the item [id] after all, while its deletion still waits.
|
||||
void undo(String id) {
|
||||
if (_waiting.containsKey(id)) {
|
||||
_waiting.remove(id)?.cancel();
|
||||
_changed();
|
||||
}
|
||||
}
|
||||
|
||||
/// Drops the deletions still waiting, and lets those under way finish.
|
||||
Future<void> dispose() async {
|
||||
for (final timer in _waiting.values) {
|
||||
timer?.cancel();
|
||||
}
|
||||
_waiting.clear();
|
||||
await Future.wait(_deleting.values);
|
||||
await _changes.close();
|
||||
}
|
||||
|
||||
void _start(String id) {
|
||||
_waiting.remove(id);
|
||||
_deleting[id] = _guarded(id).whenComplete(() {
|
||||
_deleting.remove(id);
|
||||
_changed();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _guarded(String id) async {
|
||||
try {
|
||||
await _deleteNow(id);
|
||||
} on Object catch (error) {
|
||||
// What is left shows again; deleting it once more retries.
|
||||
log('Deleting $id failed', name: _logName, error: error);
|
||||
}
|
||||
}
|
||||
|
||||
void _changed() {
|
||||
if (!_changes.isClosed) {
|
||||
_changes.add(pending);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
packages/halkyon_core/lib/src/async/sync_requests.dart
Normal file
11
packages/halkyon_core/lib/src/async/sync_requests.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/// What a synced gateway asks of the sync that carries its writes (Learn's
|
||||
/// and the agent's alike, Halkyon's ADR 0044, D49): a sync soon after each
|
||||
/// write, and word whenever one merged changes from elsewhere. halkyon_app's
|
||||
/// `SyncService` is one; the agent's sync is another.
|
||||
abstract interface class SyncRequests() {
|
||||
/// Asks for a sync soon; several asked for together run as one.
|
||||
void requestSync();
|
||||
|
||||
/// An event each time a sync merged changes from elsewhere.
|
||||
Stream<void> get merges;
|
||||
}
|
||||
Loading…
Reference in a new issue