From 90fc31a938afac0f6910c7947c6ecba0adebfea4 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Mon, 06 Apr 2026 14:12:23 +0200
Subject: [PATCH] fix: immediate disk writes, notification tap skip same session, catch_up trace logging, resume simplification
---
lib/services/message_store.dart | 14 +++++++++++---
lib/providers/providers.dart | 10 +++++++++-
lib/screens/chat_screen.dart | 29 +++++++++++++++--------------
3 files changed, 35 insertions(+), 18 deletions(-)
diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart
index 63cf5a4..ad4259a 100644
--- a/lib/providers/providers.dart
+++ b/lib/providers/providers.dart
@@ -9,6 +9,7 @@
import '../models/server_config.dart';
import '../models/session.dart';
import '../services/message_store.dart';
+import '../services/trace_service.dart';
import '../services/mqtt_service.dart' show ConnectionStatus;
import '../services/navigate_notifier.dart';
@@ -99,6 +100,12 @@
/// Switch to a new session and load its messages.
Future<void> switchSession(String sessionId) async {
+ // Log caller for debugging
+ final trace = StackTrace.current.toString().split('\n').take(4).join(' | ');
+ TraceService.instance.addTrace(
+ 'switchSession',
+ 'from=${_currentSessionId?.substring(0, 8) ?? "null"}(${state.length}) → ${sessionId.substring(0, 8)} | $trace',
+ );
// Write current session DIRECTLY to disk (no debounce — prevents data loss)
if (_currentSessionId != null && state.isNotEmpty) {
await MessageStore.writeDirect(_currentSessionId!, state);
@@ -113,7 +120,8 @@
void addMessage(Message message) {
state = [...state, message];
if (_currentSessionId != null) {
- MessageStore.save(_currentSessionId!, state);
+ // Write immediately (not debounced) to prevent race with switchSession's loadAll
+ MessageStore.writeDirect(_currentSessionId!, state);
}
}
diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart
index e470ce7..e889469 100644
--- a/lib/screens/chat_screen.dart
+++ b/lib/screens/chat_screen.dart
@@ -223,19 +223,18 @@
// Re-register APNs token after reconnect so daemon always has a fresh token
_push?.onMqttConnected();
};
- _ws!.onResume = () async {
- // App came back from background — reload messages and catch up.
- _chatLog('onResume: reloading messages and sending catch_up');
+ _ws!.onResume = () {
+ // App came back from background. The in-memory state already has
+ // any messages received while suspended (addMessage was called).
+ // Just rebuild the UI and scroll to bottom to show them.
+ _chatLog('onResume: rebuilding UI and sending catch_up');
_sendCommand('catch_up', {'lastSeq': _lastSeq});
- // Force reload current session messages from provider (triggers rebuild)
- final activeId = ref.read(activeSessionIdProvider);
- if (activeId != null) {
- // Re-save then re-load to ensure UI matches persisted state
- await ref.read(messagesProvider.notifier).switchSession(activeId);
- }
if (mounted) {
setState(() {});
- _scrollToBottom();
+ // Scroll after the frame rebuilds
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (mounted) _scrollToBottom();
+ });
}
};
_ws!.onError = (error) {
@@ -260,11 +259,12 @@
// sent immediately if already connected.
_push = PushService(mqttService: _ws!);
_push!.onNotificationTap = (data) {
- // Switch to the session immediately, then request catch_up.
- // The MQTT connection auto-reconnects on resume, and onResume
- // already sends catch_up. We just need to be on the right session.
+ // Only switch if tapping a notification for a DIFFERENT session.
+ // If already on this session, the message is already displayed —
+ // calling switchSession would reload from disk and lose it.
final sessionId = data['sessionId'] as String?;
- if (sessionId != null && mounted) {
+ final activeId = ref.read(activeSessionIdProvider);
+ if (sessionId != null && sessionId != activeId && mounted) {
_switchSession(sessionId);
}
};
@@ -414,6 +414,7 @@
);
}
+ _chatLog('catch_up msg: session=${msgSessionId?.substring(0, 8) ?? "NULL"} active=${activeId?.substring(0, 8)} match=${msgSessionId == activeId || msgSessionId == null} content="${content.substring(0, content.length.clamp(0, 40))}"');
if (msgSessionId == null || msgSessionId == activeId) {
// Active session or no session: add directly to chat
ref.read(messagesProvider.notifier).addMessage(message);
diff --git a/lib/services/message_store.dart b/lib/services/message_store.dart
index 2d682ec..f45e0d0 100644
--- a/lib/services/message_store.dart
+++ b/lib/services/message_store.dart
@@ -6,6 +6,7 @@
import 'package:path_provider/path_provider.dart';
import '../models/message.dart';
+import 'trace_service.dart';
/// Per-session JSON file persistence with debounced saves.
class MessageStore {
@@ -59,6 +60,8 @@
/// Write directly to disk, bypassing debounce. For critical saves.
static Future<void> writeDirect(String sessionId, List<Message> messages) async {
+ // Cancel ALL pending debounce to prevent race with frozen iOS timers
+ _debounceTimer?.cancel();
_pendingSaves.remove(sessionId);
await _writeSession(sessionId, messages);
}
@@ -85,9 +88,11 @@
final file = File('${dir.path}/${_fileForSession(sessionId)}');
// Strip heavy fields for persistence
final lightMessages = messages.map((m) => m.toJsonLight()).toList();
- await file.writeAsString(jsonEncode(lightMessages));
+ final json = jsonEncode(lightMessages);
+ await file.writeAsString(json);
+ TraceService.instance.addTrace('MsgStore WRITE', '${sessionId.substring(0, 8)}: ${messages.length} msgs');
} catch (e) {
- // Silently fail - message persistence is best-effort
+ TraceService.instance.addTrace('MsgStore WRITE ERROR', '${sessionId.substring(0, 8)}: $e');
}
}
@@ -130,11 +135,14 @@
final jsonStr = await file.readAsString();
final List<dynamic> jsonList = jsonDecode(jsonStr) as List<dynamic>;
- return jsonList
+ final msgs = jsonList
.map((j) => _messageFromJson(j as Map<String, dynamic>))
.where((m) => !m.isEmptyVoice && !m.isEmptyText)
.toList();
+ TraceService.instance.addTrace('MsgStore LOAD', '${sessionId.substring(0, 8)}: ${msgs.length} msgs');
+ return msgs;
} catch (e) {
+ TraceService.instance.addTrace('MsgStore LOAD ERROR', '${sessionId.substring(0, 8)}: $e');
return [];
}
}
--
Gitblit v1.3.1