From eef101bef3b6975fca065625a4fbb465613948f7 Mon Sep 17 00:00:00 2001 From: bvandeusen Date: Sun, 1 Mar 2026 11:56:26 -0500 Subject: [PATCH] Fix bugs found in full code audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Edit screens (note, task): cache _initFuture in initState instead of calling _loadExisting() from FutureBuilder on every rebuild. Prevents race condition where multiple concurrent loads could overwrite edits. Also removes unnecessary _loaded flag and simplifies the pattern. - Note editor: remove onChanged: (_) => setState((){}) on the body TextField — triggered a full rebuild on every keystroke for no reason. - SSE streaming: use utf8.decode(chunk, allowMalformed: true) so a malformed byte sequence from the server skips rather than throwing an unhandled FormatException that escaped all catch blocks. - Chat provider: guard msgs.isEmpty before accessing msgs.last during SSE chunk processing to prevent a potential index error on empty state. - Offline queue drain: check mounted before and after each async gap in _drainQueue so ref is never accessed on a disposed widget. Co-Authored-By: Claude Sonnet 4.6 --- lib/app.dart | 5 ++++- lib/data/api/chat_api.dart | 2 +- lib/providers/chat_provider.dart | 1 + lib/screens/notes/note_edit_screen.dart | 19 +++++++++++-------- lib/screens/tasks/task_edit_screen.dart | 21 +++++++++++++-------- 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index 61f4e82..2f992a4 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -245,12 +245,15 @@ class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> { } Future _drainQueue() async { + if (!mounted) return; final queue = ref.read(captureQueueProvider); if (queue.isEmpty) return; final api = ref.read(quickCaptureApiProvider); for (final text in List.from(queue)) { + if (!mounted) break; try { final result = await api.capture(text); + if (!mounted) break; await ref.read(captureQueueProvider.notifier).dequeue(text); switch (result.type) { case 'note': @@ -263,7 +266,7 @@ class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> { break; // Still offline — stop draining. } catch (_) { // Server/parse error — remove to avoid infinite retries. - await ref.read(captureQueueProvider.notifier).dequeue(text); + if (mounted) await ref.read(captureQueueProvider.notifier).dequeue(text); } } } diff --git a/lib/data/api/chat_api.dart b/lib/data/api/chat_api.dart index 4707e16..c3dfef0 100644 --- a/lib/data/api/chat_api.dart +++ b/lib/data/api/chat_api.dart @@ -92,7 +92,7 @@ class ChatApi { String currentEvent = ''; await for (final chunk in stream) { - buf.write(utf8.decode(chunk)); + buf.write(utf8.decode(chunk, allowMalformed: true)); final raw = buf.toString(); final lines = raw.split('\n'); diff --git a/lib/providers/chat_provider.dart b/lib/providers/chat_provider.dart index 1d53d05..4b4e687 100644 --- a/lib/providers/chat_provider.dart +++ b/lib/providers/chat_provider.dart @@ -90,6 +90,7 @@ class MessagesNotifier extends FamilyAsyncNotifier, int> { await for (final chunk in repo.streamGeneration(convId)) { streamedContent = true; final msgs = state.requireValue; + if (msgs.isEmpty) continue; final updated = msgs.last.copyWith(content: msgs.last.content + chunk); state = AsyncData([...msgs.sublist(0, msgs.length - 1), updated]); } diff --git a/lib/screens/notes/note_edit_screen.dart b/lib/screens/notes/note_edit_screen.dart index 51f3fcd..1296c0f 100644 --- a/lib/screens/notes/note_edit_screen.dart +++ b/lib/screens/notes/note_edit_screen.dart @@ -20,7 +20,16 @@ class _NoteEditScreenState extends ConsumerState { final _contentController = TextEditingController(); bool _preview = false; bool _saving = false; - bool _loaded = false; + + // Future is created once in initState so FutureBuilder never restarts it. + late final Future _initFuture; + + @override + void initState() { + super.initState(); + _initFuture = + widget.noteId != null ? _loadExisting() : Future.value(); + } @override void dispose() { @@ -30,15 +39,10 @@ class _NoteEditScreenState extends ConsumerState { } Future _loadExisting() async { - if (_loaded || widget.noteId == null) { - _loaded = true; - return; - } final note = await ref.read(notesRepositoryProvider).getOne(widget.noteId!); _titleController.text = note.title; _contentController.text = note.body; - _loaded = true; } Future _delete() async { @@ -97,7 +101,7 @@ class _NoteEditScreenState extends ConsumerState { @override Widget build(BuildContext context) { return FutureBuilder( - future: _loadExisting(), + future: _initFuture, builder: (context, snapshot) { return Scaffold( appBar: AppBar( @@ -159,7 +163,6 @@ class _NoteEditScreenState extends ConsumerState { expands: true, keyboardType: TextInputType.multiline, textAlignVertical: TextAlignVertical.top, - onChanged: (_) => setState(() {}), ), ), ), diff --git a/lib/screens/tasks/task_edit_screen.dart b/lib/screens/tasks/task_edit_screen.dart index 1a87f79..9a164ce 100644 --- a/lib/screens/tasks/task_edit_screen.dart +++ b/lib/screens/tasks/task_edit_screen.dart @@ -23,7 +23,16 @@ class _TaskEditScreenState extends ConsumerState { TaskPriority _priority = TaskPriority.medium; DateTime? _dueDate; bool _saving = false; - bool _loaded = false; + + // Future is created once in initState so FutureBuilder never restarts it. + late final Future _initFuture; + + @override + void initState() { + super.initState(); + _initFuture = + widget.taskId != null ? _loadExisting() : Future.value(); + } @override void dispose() { @@ -33,17 +42,13 @@ class _TaskEditScreenState extends ConsumerState { } Future _loadExisting() async { - if (_loaded || widget.taskId == null) { - _loaded = true; - return; - } - final task = await ref.read(tasksRepositoryProvider).getOne(widget.taskId!); + final task = + await ref.read(tasksRepositoryProvider).getOne(widget.taskId!); _titleController.text = task.title; _descController.text = task.description ?? ''; _status = task.status; _priority = task.priority; _dueDate = task.dueDate; - _loaded = true; } Future _save() async { @@ -117,7 +122,7 @@ class _TaskEditScreenState extends ConsumerState { @override Widget build(BuildContext context) { return FutureBuilder( - future: _loadExisting(), + future: _initFuture, builder: (context, snapshot) { return Scaffold( appBar: AppBar(