todo-app/lib/sync/local_backup.dart
Krzysztof kuhy Rudnicki f463e50d67 Switch to very_good_analysis, matching the rest of the Flutter ecosystem
Standardizes on the same stricter lint set + shared analysis_options.yaml
used by diet-guard/app and the new habit_stack app. Fixes all 127 issues
surfaced: dart fix --apply for imports/cascades/sorting, unawaited() on
fire-and-forget Futures, bare catch(e) narrowed to on Exception catch(e)
(matching diet_guard's existing pattern), one documented ignore for a
deliberate TypeError catch around untrusted peer JSON, and missing
public-member doc comments throughout. No behavior change: 174/174 tests
pass, 100% line coverage maintained.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 11:05:13 +02:00

73 lines
2.6 KiB
Dart

import 'dart:async';
import 'package:todo/data/note.dart';
import 'package:todo/sync/notes_markdown.dart';
/// Keeps an always-current Markdown backup of all notes on local disk, and
/// recovers from it on launch.
///
/// This is a third durability layer alongside GitHub auto-sync and Android
/// Auto Backup: a plain human-readable file the user (or an LLM) can read
/// directly. File IO is injected ([reader]/[writer]) so the class is pure and
/// fully testable; the platform-specific path lives in the caller.
///
/// Writes are debounced so a burst of keystrokes produces a single export.
///
/// The note snapshot is pulled lazily via [fetch] only when the debounce
/// fires — never per keystroke — so serialising every note stays off the
/// typing hot path and the cost is independent of how fast the user types.
class LocalBackup {
/// Creates a [LocalBackup] wiring the note [fetch]/[reader]/[writer].
LocalBackup({
required this.fetch,
required this.reader,
required this.writer,
this.debounce = const Duration(seconds: 2),
});
/// Pulls the current notes to export. Invoked once per debounced write, so
/// the O(notes) query + serialization happens at most once per idle window.
final Future<List<Note>> Function() fetch;
/// Reads the backup file's contents, or null if it does not exist.
final Future<String?> Function() reader;
/// Writes the given Markdown to the backup file (overwriting it).
final Future<void> Function(String markdown) writer;
/// How long to wait after the last change before writing.
final Duration debounce;
Timer? _timer;
/// Schedules a debounced backup write. Repeated calls reset the timer, so a
/// burst of writes collapses to one export of the latest snapshot. A zero
/// [debounce] exports immediately (and schedules no timer).
void schedule() {
_timer?.cancel();
if (debounce == Duration.zero) {
unawaited(_export());
} else {
_timer = Timer(debounce, () => unawaited(_export()));
}
}
/// Pulls the latest notes and writes the Markdown backup. Returned so tests
/// (and a zero-debounce caller) can await the write.
Future<void> _export() async {
final notes = await fetch();
await writer(NotesMarkdown.export(notes));
}
/// Reads the backup file and parses it into notes for recovery. Returns an
/// empty list when there is no (usable) backup.
Future<List<Note>> recover() async {
final contents = await reader();
if (contents == null || contents.trim().isEmpty) return const [];
return NotesMarkdown.parse(contents);
}
/// Cancels any pending write. Call from the owner's dispose.
void dispose() => _timer?.cancel();
}