Skip to content

fix: persist trusted node metadata across app restarts#448

Closed
mostronator wants to merge 4 commits intomainfrom
fix/persist-trusted-node-metadata
Closed

fix: persist trusted node metadata across app restarts#448
mostronator wants to merge 4 commits intomainfrom
fix/persist-trusted-node-metadata

Conversation

@mostronator
Copy link
Collaborator

@mostronator mostronator commented Feb 12, 2026

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.trustedMostroNodes on every init() call, which only provides pubkey and name. Kind 0 metadata fetched at runtime via updateNodeMetadata() was persisted only for custom nodes (via _saveCustomNodes()), leaving trusted node metadata in memory only.

Trusted: Config (pubkey+name) → fetch kind 0 → memory only → LOST on restart
Custom:  SharedPreferences → fetch kind 0 → SharedPreferences → RESTORED ✅

Solution

Add a unified metadata cache (mostroNodeMetadataCache in SharedPreferences) that stores kind 0 metadata for all nodes, keyed by pubkey.

Flow after fix:

  1. init() loads the metadata cache
  2. Trusted nodes merge cached metadata (picture, about, website) with Config values
  3. If no cache exists, falls back to Config name (backward compatible)
  4. updateNodeMetadata() updates the cache for every node on kind 0 fetch
  5. On next restart, cached metadata is immediately available

Changes

  • storage_keys.dart: Add mostroNodeMetadataCache key
  • mostro_nodes_notifier.dart:
    • init(): Load cache and merge into trusted nodes
    • updateNodeMetadata(): Also persist to metadata cache
    • _loadMetadataCache() / _updateMetadataCache(): New cache helpers
  • Tests: 3 new tests covering cache restore, persistence, and fallback

Backward compatibility

  • First run after update: cache is empty, behavior identical to before (Config name + relay fetch)
  • After first successful kind 0 fetch: metadata cached for future restarts
  • Custom nodes: unaffected (still use their own persistence via _saveCustomNodes())

Summary by CodeRabbit

  • New Features

    • Persistent caching of Mostro node metadata (name, picture, website, about); cached details are restored on startup and persisted when updated.
  • Bug Fixes / Migration

    • Removes duplicate node entries and auto-imports missing valid node entries to keep node lists consistent.
  • Tests

    • Added tests covering metadata cache persistence, restoration, and fallback to configured values.

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.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

Walkthrough

Adds 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

Cohort / File(s) Summary
Storage keys
lib/data/models/enums/storage_keys.dart
Added SharedPreferencesKeys.mostroNodeMetadataCache enum value to store the node metadata cache.
Mostro node metadata + persistence
lib/features/mostro/mostro_nodes_notifier.dart
Introduced in-memory metadata cache, _loadMetadataCache() and _updateMetadataCache() helpers, load/merge cached metadata during init(), persist metadata updates, remove overlapping custom nodes, auto-import current pubkey, and added metadata-related logging.
Tests
test/features/mostro/mostro_nodes_notifier_test.dart
Added tests for metadata cache persistence, restoration on init, update persistence, and fallback to config values when cache is absent.
Config
lib/core/config.dart
Added about, picture, and website fields to trustedMostroNodes entry for the default node.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • MostroP2P/mobile#437 — Adds metadata fetching/apply logic that populates or uses the metadata cache introduced here.
  • MostroP2P/mobile#443 — Modifies mostro_nodes_notifier.dart (pubkey normalization and node selection) and may interact with the notifier changes.
  • MostroP2P/mobile#436 — Introduced initial Mostro node management and storage-key definitions that this PR extends.

Suggested reviewers

  • AndreaDiazCorreia
  • BraCR10

Poem

🐰
I hopped through prefs to save each name,
So trusted nodes remember their fame.
Picture, website, about held tight —
No more blank starts in the morning light.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: persisting trusted node metadata across app restarts, which is the core focus of all modifications.
Linked Issues check ✅ Passed All primary objectives from issue #447 are implemented: metadata persistence in SharedPreferences, cache loading on init, metadata merging with Config fallback, and persistence on updates.
Out of Scope Changes check ✅ Passed All changes are directly related to issue #447 requirements. The enum addition, cache implementation, and config defaults all support the metadata persistence solution.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/persist-trusted-node-metadata

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Member

@Catrya Catrya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK

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.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +378 to +385
) async {
try {
_metadataCache[pubkey] = {
if (name != null) 'name': name,
if (picture != null) 'picture': picture,
if (website != null) 'website': website,
if (about != null) 'about': about,
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find MostroNode class definition and withMetadata method
fd mostro_node . --type f --extension dart | head -20

Repository: MostroP2P/mobile

Length of output: 406


🏁 Script executed:

# Search for withMetadata method definition
rg "withMetadata" --type dart -B 2 -A 10

Repository: 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 3

Repository: 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.dart

Repository: 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.dart

Repository: 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.dart

Repository: 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.dart

Repository: 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 -100

Repository: 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.dart

Repository: 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 10

Repository: 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 -100

Repository: 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 -i

Repository: 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.

@mostronator
Copy link
Collaborator Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Trusted node metadata (kind 0) is lost on app restart

2 participants