From 30ab360d66f1fbdd3b906ae1431da48723e64ca6 Mon Sep 17 00:00:00 2001 From: Strycher Date: Mon, 20 Jul 2026 04:43:42 -0400 Subject: [PATCH] fix(#306): surface silent storage failures per SAFELANE 6 The stores violated the no-silent-failures rule, and that is why tonight's diagnosis took as long as it did. Two classes: 1. Ten catch blocks swallowed decode errors and returned an empty collection. `catch (_) { return []; }` means a corrupt or unreadable store presents to the caller as "no data", so the user sees no contacts, no channels or no history with nothing anywhere explaining why. Indistinguishable from data loss. Every one now logs what failed, with the store named, before returning. 2. ChannelMessageStore._storageKey fell back from the PSK-identity key to the legacy slot-index key without a word. That fallback is correct before a channel list exists, but once history has been migrated to the PSK key it reads a DIFFERENT key and returns empty. That is exactly #333: a user's 566 Public messages were intact on disk while the app showed nothing, with no error, no parse failure and no log line to follow. It now warns when it falls back while a resolver is installed, since that combination specifically means the channel list was not loaded first. Had either of these been loud, #333 would have been a one-line log read instead of a multi-hour investigation that looked like data loss. flutter analyze clean, dart format clean, 494 tests pass. --- lib/storage/channel_message_store.dart | 14 ++++++++++++++ lib/storage/channel_store.dart | 6 +++++- lib/storage/community_store.dart | 12 ++++++++++-- lib/storage/contact_discovery_store.dart | 10 +++++++++- lib/storage/contact_store.dart | 6 +++++- lib/storage/message_store.dart | 7 +++++++ lib/storage/unread_store.dart | 6 +++++- 7 files changed, 55 insertions(+), 6 deletions(-) diff --git a/lib/storage/channel_message_store.dart b/lib/storage/channel_message_store.dart index dfef8c3..63dc24a 100644 --- a/lib/storage/channel_message_store.dart +++ b/lib/storage/channel_message_store.dart @@ -32,11 +32,25 @@ class ChannelMessageStore { String _indexKey(int channelIndex) => '$keyFor$channelIndex'; /// Active storage key: PSK identity when known, else the slot index. + /// + /// The index fallback is legitimate before a channel list has loaded, but it + /// reads a DIFFERENT key: if history was already migrated to the PSK key, the + /// caller gets an empty list that is indistinguishable from data loss. + /// SAFELANE 6 - this must never be silent. Falling back where a resolver + /// exists means the channel list was not ready, which is the #333 race. String _storageKey(int channelIndex) { final pskHex = channelPskResolver?.call(channelIndex); if (pskHex != null && pskHex.isNotEmpty) { return '$keyFor$_pskMarker$pskHex'; } + if (channelPskResolver != null) { + appLogger.warn( + 'Channel $channelIndex has no PSK yet; falling back to the slot-index ' + 'key. Any history already migrated to the PSK key will read as EMPTY. ' + 'This means the channel list was not loaded first (#333).', + tag: 'Storage', + ); + } return _indexKey(channelIndex); } diff --git a/lib/storage/channel_store.dart b/lib/storage/channel_store.dart index b52b5b7..596d3bc 100644 --- a/lib/storage/channel_store.dart +++ b/lib/storage/channel_store.dart @@ -47,7 +47,11 @@ class ChannelStore { return jsonList .map((entry) => _fromJson(entry as Map)) .toList(); - } catch (_) { + } catch (e) { + // SAFELANE 6: never swallow. A decode failure here is + // indistinguishable from 'no data' to the caller, which reads + // to the user as data loss. + appLogger.error('Failed to decode channels: $e', tag: 'Storage'); return []; } } diff --git a/lib/storage/community_store.dart b/lib/storage/community_store.dart index a8010a1..732ff47 100644 --- a/lib/storage/community_store.dart +++ b/lib/storage/community_store.dart @@ -107,7 +107,11 @@ class CommunityStore { final communities = await loadCommunities(); try { return communities.firstWhere((c) => c.id == communityId); - } catch (_) { + } catch (e) { + // SAFELANE 6: never swallow. A decode failure here is + // indistinguishable from 'no data' to the caller, which reads + // to the user as data loss. + appLogger.error('Failed to decode communities: $e', tag: 'Storage'); return null; } } @@ -118,7 +122,11 @@ class CommunityStore { final communities = await loadCommunities(); try { return communities.firstWhere((c) => c.communityId == cid); - } catch (_) { + } catch (e) { + // SAFELANE 6: never swallow. A decode failure here is + // indistinguishable from 'no data' to the caller, which reads + // to the user as data loss. + appLogger.error('Failed to decode communities: $e', tag: 'Storage'); return null; } } diff --git a/lib/storage/contact_discovery_store.dart b/lib/storage/contact_discovery_store.dart index 3f6f171..796652e 100644 --- a/lib/storage/contact_discovery_store.dart +++ b/lib/storage/contact_discovery_store.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import '../models/contact.dart'; import 'prefs_manager.dart'; +import '../utils/app_logger.dart'; class ContactDiscoveryStore { static const String _keyPrefix = 'discovered_contacts'; @@ -17,7 +18,14 @@ class ContactDiscoveryStore { return jsonList .map((entry) => _fromJson(entry as Map)) .toList(); - } catch (_) { + } catch (e) { + // SAFELANE 6: never swallow. A decode failure here is + // indistinguishable from 'no data' to the caller, which reads + // to the user as data loss. + appLogger.error( + 'Failed to decode discovered contacts: $e', + tag: 'Storage', + ); return []; } } diff --git a/lib/storage/contact_store.dart b/lib/storage/contact_store.dart index 9085d4e..9149046 100644 --- a/lib/storage/contact_store.dart +++ b/lib/storage/contact_store.dart @@ -47,7 +47,11 @@ class ContactStore { return jsonList .map((entry) => _fromJson(entry as Map)) .toList(); - } catch (_) { + } catch (e) { + // SAFELANE 6: never swallow. A decode failure here is + // indistinguishable from 'no data' to the caller, which reads + // to the user as data loss. + appLogger.error('Failed to decode contacts: $e', tag: 'Storage'); return []; } } diff --git a/lib/storage/message_store.dart b/lib/storage/message_store.dart index 6045342..96595e4 100644 --- a/lib/storage/message_store.dart +++ b/lib/storage/message_store.dart @@ -66,6 +66,13 @@ class MessageStore { final jsonList = jsonDecode(jsonString) as List; return jsonList.map((json) => _messageFromJson(json)).toList(); } catch (e) { + // SAFELANE 6: never swallow. A decode failure here is + // indistinguishable from 'no data' to the caller, which reads + // to the user as data loss. + appLogger.error( + 'Failed to decode messages for $contactKeyHex: $e', + tag: 'Storage', + ); return []; } } diff --git a/lib/storage/unread_store.dart b/lib/storage/unread_store.dart index cc1f377..a9cd4df 100644 --- a/lib/storage/unread_store.dart +++ b/lib/storage/unread_store.dart @@ -57,7 +57,11 @@ class UnreadStore { try { final json = jsonDecode(jsonString) as Map; return json.map((key, value) => MapEntry(key, value as int)); - } catch (_) { + } catch (e) { + // SAFELANE 6: never swallow. A decode failure here is + // indistinguishable from 'no data' to the caller, which reads + // to the user as data loss. + appLogger.error('Failed to decode unread state: $e', tag: 'Storage'); return {}; } }