fix: persist trusted node metadata across app restarts#448
fix: persist trusted node metadata across app restarts#448mostronator wants to merge 4 commits intomainfrom
Conversation
Trusted nodes created from Config only had pubkey and hardcoded name. Kind 0 metadata (picture, about, website) fetched at runtime was stored only in memory and lost on every app restart, making trusted nodes appear 'bare' in the node selector until a successful relay fetch. Add a unified metadata cache (SharedPreferences) that stores kind 0 metadata for all nodes. On init(), trusted nodes merge their cached metadata so last-known values are available immediately. The cache is updated whenever updateNodeMetadata() is called (from kind 0 fetches). - Add mostroNodeMetadataCache storage key - Load and merge cached metadata during init() for trusted nodes - Persist metadata for all nodes in updateNodeMetadata() - Add tests for cache restore, persistence, and fallback behavior Closes #447
updateNodeMetadata() is sync but _updateMetadataCache() is async (fire-and-forget). Add Future.delayed(Duration.zero) to let the microtask complete before verifying the setString call.
Multiple concurrent fire-and-forget _updateMetadataCache() calls from updateNodeMetadata() would each load SharedPreferences independently, causing later writes to overwrite earlier ones. This meant the trusted node's cached metadata could be lost when multiple nodes were updated in the same fetchAllNodeMetadata() cycle. Fix: load the cache once during init() into an in-memory map. All updates go through the same map instance, then persist atomically. Also add debug logging to fetchAllNodeMetadata and _applyMetadataFromEvent to help diagnose if the kind 0 fetch is returning data for trusted nodes.
WalkthroughAdds a persisted in-memory metadata cache for Mostro nodes: new SharedPreferences key, loading cached metadata during notifier init, merging cached fields into trusted nodes, persisting updates, and test coverage for cache persistence and fallback behavior. Changes
Sequence Diagram(s)sequenceDiagram
participant Init as Init Flow
participant Prefs as SharedPreferences
participant Config as Config
participant Notifier as MostroNodesNotifier
participant Fetcher as MetadataFetcher
Init->>Prefs: _loadMetadataCache()
Prefs-->>Init: cached metadata map (pubkey -> fields)
Init->>Config: read trustedMostroNodes
loop build trusted nodes
Init->>Init: merge Config entry with cached metadata
Init->>Notifier: create MostroNode(pubkey, name, picture, website, about, isTrusted)
end
Init-->>Notifier: trusted + custom nodes initialized
Note right of Notifier: Background metadata fetch may run
Notifier->>Fetcher: fetchAllNodeMetadata()
Fetcher-->>Notifier: kind 0 events
Notifier->>Notifier: updateNodeMetadata(pubkey, fields)
Notifier->>Prefs: _updateMetadataCache(pubkey, fields)
Prefs-->>Notifier: persisted
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The kind 0 relay fetch (fetchAllNodeMetadata) may silently fail due to timing issues (notifier disposed before fetch completes) or relay connectivity. Since we control the trusted nodes list, hardcode their metadata (name, picture, about, website) in Config. Init priority: metadata cache > Config defaults > null This ensures trusted nodes always display with full metadata even without any relay fetch or cache.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@lib/features/mostro/mostro_nodes_notifier.dart`:
- Around line 378-385: _updateMetadataCache currently replaces the entire
_metadataCache[pubkey] with a new map built only from non-null incoming fields,
which discards previously persisted metadata when some fields are null; change
_updateMetadataCache (the code that assigns _metadataCache[pubkey]) to merge the
incoming non-null fields into the existing cache entry (if any) rather than
replacing it—retrieve the existing Map? existing = _metadataCache[pubkey] ?? {};
create a new map copying existing and then conditionally overwrite keys
('name','picture','website','about') only when the corresponding parameters are
non-null, and assign that merged map back to _metadataCache[pubkey] so
updateNodeMetadata and MostroNode.withMetadata preserve previous values across
sessions.
| ) async { | ||
| try { | ||
| _metadataCache[pubkey] = { | ||
| if (name != null) 'name': name, | ||
| if (picture != null) 'picture': picture, | ||
| if (website != null) 'website': website, | ||
| if (about != null) 'about': about, | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find MostroNode class definition and withMetadata method
fd mostro_node . --type f --extension dart | head -20Repository: MostroP2P/mobile
Length of output: 406
🏁 Script executed:
# Search for withMetadata method definition
rg "withMetadata" --type dart -B 2 -A 10Repository: MostroP2P/mobile
Length of output: 8635
🏁 Script executed:
# Check the full context of _updateMetadataCache in the file mentioned
cat -n lib/features/mostro/mostro_nodes_notifier.dart | sed -n '370,395p'Repository: MostroP2P/mobile
Length of output: 1010
🏁 Script executed:
# Find all calls to _updateMetadataCache
rg "_updateMetadataCache" --type dart -B 3 -A 3Repository: MostroP2P/mobile
Length of output: 1217
🏁 Script executed:
# Check if there are any tests for metadata cache behavior
rg "metadataCache|_metadataCache" --type dart test/Repository: MostroP2P/mobile
Length of output: 42
🏁 Script executed:
# Look at the updateNodeMetadata method context to understand the flow
rg "updateNodeMetadata" --type dart -B 5 -A 15 lib/features/mostro/mostro_nodes_notifier.dartRepository: MostroP2P/mobile
Length of output: 1253
🏁 Script executed:
# Check how _metadataCache is loaded on app startup
rg "_metadataCache\s*=" --type dart -B 3 -A 3 lib/features/mostro/mostro_nodes_notifier.dartRepository: MostroP2P/mobile
Length of output: 707
🏁 Script executed:
# Check how metadata is loaded back from cache on app startup
rg "_loadMetadataCache" --type dart -B 2 -A 20 lib/features/mostro/mostro_nodes_notifier.dartRepository: MostroP2P/mobile
Length of output: 1802
🏁 Script executed:
# Check how custom nodes are loaded and whether they use the cache
rg "_loadCustomNodes" --type dart -B 2 -A 25 lib/features/mostro/mostro_nodes_notifier.dartRepository: MostroP2P/mobile
Length of output: 1900
🏁 Script executed:
# Check if custom nodes apply cached metadata in the init flow
cat -n lib/features/mostro/mostro_nodes_notifier.dart | sed -n '290,350p'Repository: MostroP2P/mobile
Length of output: 2309
🏁 Script executed:
# Check the full init() method to see if custom nodes get metadata applied
rg "init\(\)" --type dart -B 2 -A 60 lib/features/mostro/mostro_nodes_notifier.dart | head -100Repository: MostroP2P/mobile
Length of output: 2655
🏁 Script executed:
# Check if custom nodes get metadata applied anywhere during loading
rg "customNodes" --type dart -B 2 -A 5 lib/features/mostro/mostro_nodes_notifier.dart | grep -A 10 "state.*customNodes\|customNodes.*state"Repository: MostroP2P/mobile
Length of output: 528
🏁 Script executed:
# Search for where custom nodes might apply cached metadata
ast-grep --pattern 'customNodes.map' lib/features/mostro/mostro_nodes_notifier.dartRepository: MostroP2P/mobile
Length of output: 42
🏁 Script executed:
# Check if custom nodes ever get their metadata from _metadataCache applied
rg "cached\|_metadataCache" --type dart lib/features/mostro/mostro_nodes_notifier.dart | grep -A 2 -B 2 "custom"Repository: MostroP2P/mobile
Length of output: 42
🏁 Script executed:
# Let's trace through what happens with a custom node's metadata lifecycle
cat -n lib/features/mostro/mostro_nodes_notifier.dart | sed -n '220,280p'Repository: MostroP2P/mobile
Length of output: 2526
🏁 Script executed:
# Check if updateNodeMetadata saves custom nodes back to storage
cat -n lib/features/mostro/mostro_nodes_notifier.dart | sed -n '155,200p'Repository: MostroP2P/mobile
Length of output: 1706
🏁 Script executed:
# Get the complete updateNodeMetadata method
cat -n lib/features/mostro/mostro_nodes_notifier.dart | sed -n '189,220p'Repository: MostroP2P/mobile
Length of output: 1179
🏁 Script executed:
# Check the mostro_nodes_notifier_test.dart for any cache-related tests
rg "metadataCache\|updateNodeMetadata\|_updateMetadataCache" --type dart test/features/mostro/mostro_nodes_notifier_test.dart -B 3 -A 10Repository: MostroP2P/mobile
Length of output: 42
🏁 Script executed:
# Check what tests exist in the test file
cat test/features/mostro/mostro_nodes_notifier_test.dart | head -100Repository: MostroP2P/mobile
Length of output: 3692
🏁 Script executed:
# Look for any metadata cache tests
rg "cache\|metadata" test/features/mostro/mostro_nodes_notifier_test.dart -iRepository: MostroP2P/mobile
Length of output: 42
Cache entry replacement loses previously persisted fields when null values are passed.
_updateMetadataCache unconditionally replaces the cache entry for a pubkey, overwriting all non-null fields. If updateNodeMetadata is called with partial metadata (e.g., a kind 0 event with name but no picture), any previously cached values for that pubkey are silently dropped.
The in-memory MostroNode works around this via withMetadata() using name ?? this.name to preserve old values, so the app functions correctly during the session. However, on app restart, the cache loads incomplete data: trusted nodes fall back to Config defaults, but custom nodes have no fallback and permanently lose the metadata.
Merge with existing cached values instead of replacing:
Proposed fix — merge with existing cache entry
try {
+ final existing = _metadataCache[pubkey] ?? {};
_metadataCache[pubkey] = {
- if (name != null) 'name': name,
- if (picture != null) 'picture': picture,
- if (website != null) 'website': website,
- if (about != null) 'about': about,
+ ...existing,
+ if (name != null) 'name': name,
+ if (picture != null) 'picture': picture,
+ if (website != null) 'website': website,
+ if (about != null) 'about': about,
};🤖 Prompt for AI Agents
In `@lib/features/mostro/mostro_nodes_notifier.dart` around lines 378 - 385,
_updateMetadataCache currently replaces the entire _metadataCache[pubkey] with a
new map built only from non-null incoming fields, which discards previously
persisted metadata when some fields are null; change _updateMetadataCache (the
code that assigns _metadataCache[pubkey]) to merge the incoming non-null fields
into the existing cache entry (if any) rather than replacing it—retrieve the
existing Map? existing = _metadataCache[pubkey] ?? {}; create a new map copying
existing and then conditionally overwrite keys
('name','picture','website','about') only when the corresponding parameters are
non-null, and assign that merged map back to _metadataCache[pubkey] so
updateNodeMetadata and MostroNode.withMetadata preserve previous values across
sessions.
|
Closing in favor of a cleaner approach — will open a new PR with a simpler fix that caches trusted node metadata in SharedPreferences without modifying the existing data flow. |
Summary
Fixes trusted Mostro nodes losing their kind 0 metadata (picture, about, website) on every app restart.
Closes #447
Problem
Trusted nodes are recreated from
Config.trustedMostroNodeson everyinit()call, which only providespubkeyandname. Kind 0 metadata fetched at runtime viaupdateNodeMetadata()was persisted only for custom nodes (via_saveCustomNodes()), leaving trusted node metadata in memory only.Solution
Add a unified metadata cache (
mostroNodeMetadataCachein SharedPreferences) that stores kind 0 metadata for all nodes, keyed by pubkey.Flow after fix:
init()loads the metadata cacheupdateNodeMetadata()updates the cache for every node on kind 0 fetchChanges
storage_keys.dart: AddmostroNodeMetadataCachekeymostro_nodes_notifier.dart:init(): Load cache and merge into trusted nodesupdateNodeMetadata(): Also persist to metadata cache_loadMetadataCache()/_updateMetadataCache(): New cache helpersBackward compatibility
_saveCustomNodes())Summary by CodeRabbit
New Features
Bug Fixes / Migration
Tests