From f84bcaa7367d26b1e3edcaf58ac38b752c2b2481 Mon Sep 17 00:00:00 2001 From: Strycher Date: Mon, 3 Aug 2026 11:13:24 -0400 Subject: [PATCH] fix(#472): clear per-radio in-memory caches on reconnect Switching radios showed the previous radio's channel history (including its outgoing messages) on the new radio. The in-memory caches _channelMessages, _conversations, and _loadedConversationKeys are keyed by channel index / contact key, not by radio, and were never cleared on a switch; a new radio's empty store could not overwrite them (_loadChannelMessages only writes on a non-empty read). On-disk stores are already per-radio (device+PSK since #277), so no re-keying or migration is needed. Clear the three caches in _resetConnectionHandshakeState (runs at the start of every connect). loadAllChannelMessages and _loadMessagesForContact repopulate from the new radio's store. Adds a regression test. Co-Authored-By: Claude Opus 4.8 --- lib/connector/meshcore_connector.dart | 23 +++++++ test/connector/channel_cache_reset_test.dart | 68 ++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 test/connector/channel_cache_reset_test.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index e4c301e..d11a3ba 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -2789,11 +2789,34 @@ class MeshCoreConnector extends ChangeNotifier { _selfInfoRetryTimer?.cancel(); _selfInfoRetryTimer = null; _hasReceivedDeviceInfo = false; + // Drop the previous radio's in-memory history before a new connection loads + // its own. These caches are keyed by channel index / contact key, not by + // radio, so without this a radio switch would keep showing the prior + // radio's channel and DM history (a new radio's empty store cannot + // overwrite them). On-disk stores are already per-radio (device+PSK); this + // is purely the runtime cache. Repopulated on connect by + // loadAllChannelMessages and _loadMessagesForContact. (#472) + _channelMessages.clear(); + _conversations.clear(); + _loadedConversationKeys.clear(); _resetSyncProgressState(); _bleInitialSyncStarted = false; _pathHashByteWidth = 1; } + @visibleForTesting + void resetConnectionHandshakeStateForTest() => + _resetConnectionHandshakeState(); + + @visibleForTesting + Map> get channelMessagesForTest => _channelMessages; + + @visibleForTesting + Map> get conversationsForTest => _conversations; + + @visibleForTesting + Set get loadedConversationKeysForTest => _loadedConversationKeys; + void _resetSyncProgressState() { _pendingInitialChannelSync = false; _pendingInitialContactsSync = false; diff --git a/test/connector/channel_cache_reset_test.dart b/test/connector/channel_cache_reset_test.dart new file mode 100644 index 0000000..3a341ac --- /dev/null +++ b/test/connector/channel_cache_reset_test.dart @@ -0,0 +1,68 @@ +// #472: switching radios must not show the previous radio's history. +// +// The per-radio in-memory caches (_channelMessages, _conversations, +// _loadedConversationKeys) are keyed by channel index / contact key, not by +// radio. _resetConnectionHandshakeState() runs at the start of every +// connection, so it must clear them; otherwise a new radio whose store is empty +// for a channel cannot overwrite the stale entry and keeps rendering the prior +// radio's history (including its outgoing messages). On-disk stores are already +// per-radio (device+PSK), so this is purely the runtime cache. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/connector/meshcore_connector.dart'; +import 'package:meshcore_open/models/channel_message.dart'; +import 'package:meshcore_open/storage/prefs_manager.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() async { + SharedPreferences.setMockInitialValues({}); + PrefsManager.reset(); + await PrefsManager.initialize(); + }); + + test( + 'connection reset clears the previous radio in-memory caches (#472)', + () { + final connector = MeshCoreConnector(); + + // Seed the caches as if a prior radio's history had loaded. + connector.channelMessagesForTest[0] = [ + ChannelMessage( + senderName: 'PrevRadio', + text: 'history from the other radio', + timestamp: DateTime.fromMillisecondsSinceEpoch(1000), + isOutgoing: true, + status: ChannelMessageStatus.sent, + ), + ]; + connector.conversationsForTest['deadbeef00'] = []; + connector.loadedConversationKeysForTest.add('deadbeef00'); + + expect(connector.channelMessagesForTest, isNotEmpty); + expect(connector.conversationsForTest, isNotEmpty); + expect(connector.loadedConversationKeysForTest, isNotEmpty); + + // A new connection begins. + connector.resetConnectionHandshakeStateForTest(); + + expect( + connector.channelMessagesForTest, + isEmpty, + reason: 'channel history cache must be dropped on reconnect', + ); + expect( + connector.conversationsForTest, + isEmpty, + reason: 'DM conversation cache must be dropped on reconnect', + ); + expect( + connector.loadedConversationKeysForTest, + isEmpty, + reason: 'loaded-conversation markers must be dropped so DMs reload', + ); + }, + ); +}