From 46dd99be0354e1e3f92d8116866343fb986e32de Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Nov 2025 14:27:13 +0000 Subject: [PATCH 1/4] Fix unknown EOSE subscription ID warnings This fix addresses the "got unknown eose subid" warnings that were appearing in the logs when EOSE (End Of Stored Events) messages arrived from relays. The issue was that when `TimelineSub::try_add_remote()` and `TimelineSub::try_add_remote_with_relay()` created new subscription IDs, they were not being tracked in the `Subscriptions.subs` HashMap. When EOSE messages arrived for these subscription IDs, the `handle_eose()` function couldn't find them in the HashMap, causing the "unknown eose subid" warnings. Changes: - Modified `try_add_remote()` and `try_add_remote_with_relay()` to accept `&mut Subscriptions` and `&TimelineKind` parameters - Added subscription tracking by inserting subscription IDs with `SubKind::Timeline(timeline_kind)` into the Subscriptions HashMap - Updated all call sites throughout the codebase to pass the required parameters, including: - TimelineCache::open() - DecksCache::add_deck_default() - DecksCache::new_with_demo_config() - is_timeline_ready() - execute_note_action() - execute_and_process_note_action() - add_demo_columns() - demo_decks() This ensures all subscription IDs are properly tracked, eliminating the unknown EOSE warnings and allowing proper handling of EOSE messages. --- crates/notedeck_chrome/src/chrome.rs | 2 ++ crates/notedeck_columns/src/actionbar.rs | 7 +++++-- crates/notedeck_columns/src/app.rs | 7 +++++-- crates/notedeck_columns/src/column.rs | 3 ++- crates/notedeck_columns/src/decks.rs | 11 ++++++++--- crates/notedeck_columns/src/multi_subscriber.rs | 10 +++++++--- crates/notedeck_columns/src/nav.rs | 3 ++- crates/notedeck_columns/src/timeline/cache.rs | 5 +++-- crates/notedeck_columns/src/timeline/mod.rs | 5 +++-- 9 files changed, 37 insertions(+), 16 deletions(-) diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs index 29bf7ee..fc7e946 100644 --- a/crates/notedeck_chrome/src/chrome.rs +++ b/crates/notedeck_chrome/src/chrome.rs @@ -471,6 +471,7 @@ fn chrome_handle_app_action( let m_action = notedeck_columns::actionbar::execute_and_process_note_action( note_action, ctx.ndb, + &mut columns.subscriptions, cols, 0, &mut columns.timeline_cache, @@ -527,6 +528,7 @@ fn columns_route_to_profile( let m_action = notedeck_columns::actionbar::execute_and_process_note_action( notedeck::NoteAction::Profile(*pk), ctx.ndb, + &mut columns.subscriptions, cols, 0, &mut columns.timeline_cache, diff --git a/crates/notedeck_columns/src/actionbar.rs b/crates/notedeck_columns/src/actionbar.rs index a2bd5b1..891ec99 100644 --- a/crates/notedeck_columns/src/actionbar.rs +++ b/crates/notedeck_columns/src/actionbar.rs @@ -48,6 +48,7 @@ struct NoteActionResponse { fn execute_note_action( action: NoteAction, ndb: &mut Ndb, + subs: &mut crate::subscriptions::Subscriptions, timeline_cache: &mut TimelineCache, threads: &mut Threads, note_cache: &mut NoteCache, @@ -97,7 +98,7 @@ fn execute_note_action( let kind = TimelineKind::Profile(pubkey); router_action = Some(RouterAction::route_to(Route::Timeline(kind.clone()))); timeline_res = timeline_cache - .open(ndb, note_cache, txn, pool, &kind) + .open(subs, ndb, note_cache, txn, pool, &kind) .map(NotesOpenResult::Timeline); } NoteAction::Note { @@ -134,7 +135,7 @@ fn execute_note_action( let kind = TimelineKind::Hashtag(vec![htag.clone()]); router_action = Some(RouterAction::route_to(Route::Timeline(kind.clone()))); timeline_res = timeline_cache - .open(ndb, note_cache, txn, pool, &kind) + .open(subs, ndb, note_cache, txn, pool, &kind) .map(NotesOpenResult::Timeline); } NoteAction::Repost(note_id) => { @@ -222,6 +223,7 @@ fn execute_note_action( pub fn execute_and_process_note_action( action: NoteAction, ndb: &mut Ndb, + subs: &mut crate::subscriptions::Subscriptions, columns: &mut Columns, col: usize, timeline_cache: &mut TimelineCache, @@ -250,6 +252,7 @@ pub fn execute_and_process_note_action( let resp = execute_note_action( action, ndb, + subs, timeline_cache, threads, note_cache, diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs index dac9122..de44865 100644 --- a/crates/notedeck_columns/src/app.rs +++ b/crates/notedeck_columns/src/app.rs @@ -152,6 +152,7 @@ fn try_process_event( for (kind, timeline) in &mut damus.timeline_cache { let is_ready = timeline::is_timeline_ready( + &mut damus.subscriptions, app_ctx.ndb, app_ctx.pool, app_ctx.note_cache, @@ -469,6 +470,7 @@ impl Damus { parsed_args.is_flag_set(ColumnsFlag::SinceOptimize), ); + let mut subscriptions = Subscriptions::default(); let decks_cache = if tmp_columns { info!("DecksCache: loading from command line arguments"); let mut columns: Columns = Columns::new(); @@ -476,6 +478,7 @@ impl Damus { for col in &parsed_args.columns { let timeline_kind = col.clone().into_timeline_kind(); if let Some(add_result) = columns.add_new_timeline_column( + &mut subscriptions, &mut timeline_cache, &txn, app_context.ndb, @@ -507,7 +510,7 @@ impl Damus { decks_cache } else { info!("DecksCache: creating new with demo configuration"); - DecksCache::new_with_demo_config(&mut timeline_cache, app_context) + DecksCache::new_with_demo_config(&mut subscriptions, &mut timeline_cache, app_context) //for (pk, _) in &app_context.accounts.cache { // cache.add_deck_default(*pk); //} @@ -519,7 +522,7 @@ impl Damus { let threads = Threads::default(); Self { - subscriptions: Subscriptions::default(), + subscriptions, timeline_cache, drafts: Drafts::default(), state: DamusState::Initializing, diff --git a/crates/notedeck_columns/src/column.rs b/crates/notedeck_columns/src/column.rs index dfb7a91..4c4dc9c 100644 --- a/crates/notedeck_columns/src/column.rs +++ b/crates/notedeck_columns/src/column.rs @@ -103,6 +103,7 @@ impl Columns { pub fn add_new_timeline_column( &mut self, + subs: &mut crate::subscriptions::Subscriptions, timeline_cache: &mut TimelineCache, txn: &Transaction, ndb: &Ndb, @@ -112,7 +113,7 @@ impl Columns { ) -> Option { self.columns .push(Column::new(vec![Route::timeline(kind.to_owned())])); - timeline_cache.open(ndb, note_cache, txn, pool, kind) + timeline_cache.open(subs, ndb, note_cache, txn, pool, kind) } pub fn new_column_picker(&mut self) { diff --git a/crates/notedeck_columns/src/decks.rs b/crates/notedeck_columns/src/decks.rs index fc48a0b..470baab 100644 --- a/crates/notedeck_columns/src/decks.rs +++ b/crates/notedeck_columns/src/decks.rs @@ -80,12 +80,12 @@ impl DecksCache { } } - pub fn new_with_demo_config(timeline_cache: &mut TimelineCache, ctx: &mut AppContext) -> Self { + pub fn new_with_demo_config(subs: &mut crate::subscriptions::Subscriptions, timeline_cache: &mut TimelineCache, ctx: &mut AppContext) -> Self { let mut account_to_decks: HashMap = Default::default(); let fallback_pubkey = FALLBACK_PUBKEY(); account_to_decks.insert( fallback_pubkey, - demo_decks(fallback_pubkey, timeline_cache, ctx), + demo_decks(fallback_pubkey, subs, timeline_cache, ctx), ); DecksCache::new(account_to_decks, ctx.i18n) } @@ -117,6 +117,7 @@ impl DecksCache { pub fn add_deck_default( &mut self, ctx: &mut AppContext, + subs: &mut crate::subscriptions::Subscriptions, timeline_cache: &mut TimelineCache, pubkey: Pubkey, ) { @@ -125,6 +126,7 @@ impl DecksCache { // add home and notifications for new accounts add_demo_columns( ctx, + subs, timeline_cache, pubkey, &mut decks.decks_mut()[0].columns, @@ -444,6 +446,7 @@ impl Deck { pub fn add_demo_columns( ctx: &mut AppContext, + subs: &mut crate::subscriptions::Subscriptions, timeline_cache: &mut TimelineCache, pubkey: Pubkey, columns: &mut Columns, @@ -457,6 +460,7 @@ pub fn add_demo_columns( for kind in &timeline_kinds { if let Some(results) = columns.add_new_timeline_column( + subs, timeline_cache, &txn, ctx.ndb, @@ -477,13 +481,14 @@ pub fn add_demo_columns( pub fn demo_decks( demo_pubkey: Pubkey, + subs: &mut crate::subscriptions::Subscriptions, timeline_cache: &mut TimelineCache, ctx: &mut AppContext, ) -> Decks { let deck = { let mut columns = Columns::default(); - add_demo_columns(ctx, timeline_cache, demo_pubkey, &mut columns); + add_demo_columns(ctx, subs, timeline_cache, demo_pubkey, &mut columns); //columns.add_new_timeline_column(Timeline::hashtag("introductions".to_string())); diff --git a/crates/notedeck_columns/src/multi_subscriber.rs b/crates/notedeck_columns/src/multi_subscriber.rs index 567052c..c100263 100644 --- a/crates/notedeck_columns/src/multi_subscriber.rs +++ b/crates/notedeck_columns/src/multi_subscriber.rs @@ -5,7 +5,7 @@ use nostrdb::{Ndb, Subscription}; use notedeck::{filter::HybridFilter, UnifiedSubscription}; use uuid::Uuid; -use crate::{subscriptions, timeline::ThreadSelection}; +use crate::{subscriptions::{self, SubKind, Subscriptions}, timeline::{ThreadSelection, TimelineKind}}; type RootNoteId = NoteId; @@ -383,15 +383,17 @@ impl TimelineSub { ); } - pub fn try_add_remote(&mut self, pool: &mut RelayPool, filter: &HybridFilter) { - self.try_add_remote_with_relay(pool, filter, None); + pub fn try_add_remote(&mut self, subs: &mut Subscriptions, pool: &mut RelayPool, filter: &HybridFilter, timeline_kind: &TimelineKind) { + self.try_add_remote_with_relay(subs, pool, filter, None, timeline_kind); } pub fn try_add_remote_with_relay( &mut self, + subs: &mut Subscriptions, pool: &mut RelayPool, filter: &HybridFilter, relay_url: Option<&str>, + timeline_kind: &TimelineKind, ) { let before = self.state.clone(); match &mut self.state { @@ -402,6 +404,7 @@ impl TimelineSub { } else { pool.subscribe(subid.clone(), filter.remote().to_vec()); } + subs.subs.insert(subid.clone(), SubKind::Timeline(timeline_kind.clone())); self.filter = Some(filter.to_owned()); self.state = SubState::RemoteOnly { remote: subid, @@ -415,6 +418,7 @@ impl TimelineSub { } else { pool.subscribe(subid.clone(), filter.remote().to_vec()); } + subs.subs.insert(subid.clone(), SubKind::Timeline(timeline_kind.clone())); self.filter = Some(filter.to_owned()); self.state = SubState::Unified { unified: UnifiedSubscription { diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs index 558f664..a205e05 100644 --- a/crates/notedeck_columns/src/nav.rs +++ b/crates/notedeck_columns/src/nav.rs @@ -113,7 +113,7 @@ impl SwitchingAction { } if switch_action.switching_to_new { - decks_cache.add_deck_default(ctx, timeline_cache, switch_action.switch_to); + decks_cache.add_deck_default(ctx, subs, timeline_cache, switch_action.switch_to); } // pop nav after switch @@ -487,6 +487,7 @@ fn process_render_nav_action( crate::actionbar::execute_and_process_note_action( note_action, ctx.ndb, + &mut app.subscriptions, get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache), col, &mut app.timeline_cache, diff --git a/crates/notedeck_columns/src/timeline/cache.rs b/crates/notedeck_columns/src/timeline/cache.rs index cf20e06..e212727 100644 --- a/crates/notedeck_columns/src/timeline/cache.rs +++ b/crates/notedeck_columns/src/timeline/cache.rs @@ -175,6 +175,7 @@ impl TimelineCache { /// subscription pub fn open( &mut self, + subs: &mut crate::subscriptions::Subscriptions, ndb: &Ndb, note_cache: &mut NoteCache, txn: &Transaction, @@ -224,10 +225,10 @@ impl TimelineCache { // Check if this is a relay-specific timeline match &timeline.kind { TimelineKind::Relay(relay_url, _) => { - timeline.subscription.try_add_remote_with_relay(pool, filter, Some(relay_url)); + timeline.subscription.try_add_remote_with_relay(subs, pool, filter, Some(relay_url), &timeline.kind); } _ => { - timeline.subscription.try_add_remote(pool, filter); + timeline.subscription.try_add_remote(subs, pool, filter, &timeline.kind); } } } else { diff --git a/crates/notedeck_columns/src/timeline/mod.rs b/crates/notedeck_columns/src/timeline/mod.rs index ca7e72c..90eec7d 100644 --- a/crates/notedeck_columns/src/timeline/mod.rs +++ b/crates/notedeck_columns/src/timeline/mod.rs @@ -562,7 +562,7 @@ pub fn setup_new_timeline( unknown_ids: &mut UnknownIds, ) { // if we're ready, setup local subs - if is_timeline_ready(ndb, pool, note_cache, timeline, accounts, unknown_ids) { + if is_timeline_ready(subs, ndb, pool, note_cache, timeline, accounts, unknown_ids) { if let Err(err) = setup_timeline_nostrdb_sub(ndb, txn, note_cache, timeline, unknown_ids) { error!("setup_new_timeline: {err}"); } @@ -783,6 +783,7 @@ fn setup_timeline_nostrdb_sub( /// example, when we have to fetch a contact list before we do the actual /// following list query. pub fn is_timeline_ready( + subs: &mut crate::subscriptions::Subscriptions, ndb: &Ndb, pool: &mut RelayPool, note_cache: &mut NoteCache, @@ -870,7 +871,7 @@ pub fn is_timeline_ready( //let ck = &timeline.kind; //let subid = damus.gen_subid(&SubKind::Column(ck.clone())); - timeline.subscription.try_add_remote(pool, &filter); + timeline.subscription.try_add_remote(subs, pool, &filter, &timeline.kind); true } } From e2129344bc80c4809e94fafde05d6003816e7067 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Nov 2025 14:43:37 +0000 Subject: [PATCH 2/4] Fix relay URL parsing by URL-encoding relay URLs The relay URL "wss://wot.nostr.net" was being incorrectly parsed as Relay("wss", Some(["//wot.nostr.net"])) when the app restarted and loaded from the decks cache. Root cause: The TokenWriter uses ":" as the default delimiter. When relay URLs containing ":" (like "wss://wot.nostr.net") were serialized, the colons were not escaped, causing the URL to be split into multiple tokens during parsing: - Token 1: "relay" (type) - Token 2: "wss" (mistakenly read as relay_url) - Token 3: "//wot.nostr.net" (mistakenly read as hashtags) Solution: - URL-encode relay URLs when serializing (using urlencoding crate) - URL-decode relay URLs when parsing - Added backward compatibility: if decoding fails (old format), fall back to using the token as-is This ensures relay URLs with colons are correctly preserved across app restarts. --- crates/notedeck_columns/src/timeline/kind.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/notedeck_columns/src/timeline/kind.rs b/crates/notedeck_columns/src/timeline/kind.rs index 2a4496f..280359b 100644 --- a/crates/notedeck_columns/src/timeline/kind.rs +++ b/crates/notedeck_columns/src/timeline/kind.rs @@ -371,7 +371,8 @@ impl TimelineKind { } TimelineKind::Relay(relay_url, hashtags) => { writer.write_token("relay"); - writer.write_token(relay_url); + // URL-encode the relay URL to avoid issues with the ":" delimiter + writer.write_token(&urlencoding::encode(relay_url)); if let Some(ht) = hashtags { writer.write_token(&ht.join(" ")); } else { @@ -445,7 +446,12 @@ impl TimelineKind { }, |p| { p.parse_token("relay")?; - let relay_url = p.pull_token()?.to_string(); + let encoded_relay_url = p.pull_token()?; + // Try to URL-decode the relay URL (for new format) + // If decoding fails, use the token as-is (backward compatibility with old format) + let relay_url = urlencoding::decode(encoded_relay_url) + .map(|s| s.to_string()) + .unwrap_or_else(|_| encoded_relay_url.to_string()); let hashtags_str = p.pull_token()?; let hashtags = if hashtags_str.is_empty() { None From 9e2331fa3d21203f737097424e95df3911e4c360 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Nov 2025 14:50:55 +0000 Subject: [PATCH 3/4] Add migration logic to fix corrupted relay URLs from old format When decks_cache.json was saved with the old format, relay URLs like "wss://wot.nostr.net" were split by the ":" delimiter into separate tokens: ["wss", "//wot.nostr.net"]. This commit adds migration logic to detect and reconstruct these corrupted URLs during parsing. Detection heuristic: - If the relay_url token is just "wss" or "ws" (without "://") - AND the next token (hashtags_str) starts with "//" - Then reconstruct the full URL as "wss://..." or "ws://..." - And pull the next token as the actual hashtags This allows existing users with corrupted decks_cache.json files to automatically have their relay URLs fixed on next app restart, without needing to manually delete the cache file. --- crates/notedeck_columns/src/timeline/kind.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/notedeck_columns/src/timeline/kind.rs b/crates/notedeck_columns/src/timeline/kind.rs index 280359b..1f5093c 100644 --- a/crates/notedeck_columns/src/timeline/kind.rs +++ b/crates/notedeck_columns/src/timeline/kind.rs @@ -447,12 +447,25 @@ impl TimelineKind { |p| { p.parse_token("relay")?; let encoded_relay_url = p.pull_token()?; + // Try to URL-decode the relay URL (for new format) // If decoding fails, use the token as-is (backward compatibility with old format) - let relay_url = urlencoding::decode(encoded_relay_url) + let mut relay_url = urlencoding::decode(encoded_relay_url) .map(|s| s.to_string()) .unwrap_or_else(|_| encoded_relay_url.to_string()); - let hashtags_str = p.pull_token()?; + + let mut hashtags_str = p.pull_token()?; + + // Migration: Fix broken relay URLs from old format where the colon delimiter + // corrupted URLs like "wss://relay.com" into "wss" + "//relay.com" + // If relay URL looks broken (just "wss" or "ws") and next token starts with "//", + // reconstruct the full URL + if (relay_url == "wss" || relay_url == "ws") && hashtags_str.starts_with("//") { + relay_url = format!("{}:{}", relay_url, hashtags_str); + // Pull the actual hashtags token now + hashtags_str = p.pull_token().unwrap_or(""); + } + let hashtags = if hashtags_str.is_empty() { None } else { From a02e98a64b9913fd5d21e62ad369f410350573c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Nov 2025 15:02:16 +0000 Subject: [PATCH 4/4] Remove migration logic for corrupted relay URLs Since users can delete the corrupted decks_cache.json file, the migration logic is no longer necessary. The URL encoding/decoding ensures all new cache files will be saved correctly going forward. --- crates/notedeck_columns/src/timeline/kind.rs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/crates/notedeck_columns/src/timeline/kind.rs b/crates/notedeck_columns/src/timeline/kind.rs index 1f5093c..6bfa2dd 100644 --- a/crates/notedeck_columns/src/timeline/kind.rs +++ b/crates/notedeck_columns/src/timeline/kind.rs @@ -447,25 +447,12 @@ impl TimelineKind { |p| { p.parse_token("relay")?; let encoded_relay_url = p.pull_token()?; - - // Try to URL-decode the relay URL (for new format) - // If decoding fails, use the token as-is (backward compatibility with old format) - let mut relay_url = urlencoding::decode(encoded_relay_url) + // URL-decode the relay URL + let relay_url = urlencoding::decode(encoded_relay_url) .map(|s| s.to_string()) .unwrap_or_else(|_| encoded_relay_url.to_string()); - let mut hashtags_str = p.pull_token()?; - - // Migration: Fix broken relay URLs from old format where the colon delimiter - // corrupted URLs like "wss://relay.com" into "wss" + "//relay.com" - // If relay URL looks broken (just "wss" or "ws") and next token starts with "//", - // reconstruct the full URL - if (relay_url == "wss" || relay_url == "ws") && hashtags_str.starts_with("//") { - relay_url = format!("{}:{}", relay_url, hashtags_str); - // Pull the actual hashtags token now - hashtags_str = p.pull_token().unwrap_or(""); - } - + let hashtags_str = p.pull_token()?; let hashtags = if hashtags_str.is_empty() { None } else {