notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 22d9b2bc48b6b8264abdb1ee8e8bc6f92a1baa9e
parent c7b7fb9ec1f5697bbf65bd53ef9a11381c44b1e3
Author: kernelkind <kernelkind@gmail.com>
Date:   Mon, 23 Feb 2026 23:20:13 -0500

feat(scoped-subs): add `RemoteApi`

Signed-off-by: kernelkind <kernelkind@gmail.com>

Diffstat:
Mcrates/notedeck/src/lib.rs | 2++
Acrates/notedeck/src/remote_api.rs | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 154 insertions(+), 0 deletions(-)

diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs @@ -32,6 +32,7 @@ pub mod profile; mod publish; pub mod relay_debug; pub mod relayspec; +mod remote_api; mod result; mod route; mod scoped_sub_api; @@ -96,6 +97,7 @@ pub use profile::*; pub use publish::{AccountsPublishApi, ExplicitPublishApi, PublishApi, RelayType}; pub use relay_debug::RelayDebugView; pub use relayspec::RelaySpec; +pub use remote_api::{RelayInspectApi, RelayInspectEntry, RemoteApi}; pub use result::Result; pub use route::{DrawerRouter, ReplacementType, Router}; pub use scoped_sub_api::ScopedSubApi; diff --git a/crates/notedeck/src/remote_api.rs b/crates/notedeck/src/remote_api.rs @@ -0,0 +1,152 @@ +use egui::Context; +use enostr::{NormRelayUrl, OutboxSession, Pubkey, RelayImplType, RelayStatus}; +use nostrdb::Ndb; + +use crate::{ + Accounts, ExplicitPublishApi, OneshotApi, Outbox, PublishApi, ScopedSubApi, ScopedSubsState, +}; + +/// Read-only relay inspection row for relay UI surfaces. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RelayInspectEntry<'a> { + pub relay_url: &'a NormRelayUrl, + pub status: RelayStatus, +} + +/// Read-only relay inspection facade. +/// +/// This exposes only relay status inspection needed by UI code and intentionally +/// does not provide subscription/publish/oneshot methods. +pub struct RelayInspectApi<'r, 'a> { + pool: &'r Outbox<'a>, +} + +impl<'r, 'a> RelayInspectApi<'r, 'a> { + pub(crate) fn new(pool: &'r Outbox<'a>) -> Self { + Self { pool } + } + + /// Snapshot websocket relay statuses for display/debug UI. + pub fn relay_infos(&self) -> Vec<RelayInspectEntry<'_>> { + self.pool + .outbox + .websocket_statuses() + .into_iter() + .map(|(url, status)| RelayInspectEntry { + relay_url: url, + status, + }) + .collect() + } +} + +/// App-facing facade for relay/outbox transport operations. +/// +/// This is the only app-visible entrypoint for scoped subscriptions, one-shot +/// requests, publishing, relay event ingestion, and relay status inspection. +/// Apps should not access raw `Outbox` directly. +pub struct RemoteApi<'a> { + pool: Outbox<'a>, + scoped_sub_state: &'a mut ScopedSubsState, +} + +impl<'a> RemoteApi<'a> { + pub(crate) fn new(pool: Outbox<'a>, scoped_sub_state: &'a mut ScopedSubsState) -> Self { + Self { + pool, + scoped_sub_state, + } + } + + /// Export the staged outbox session without exposing the raw handler. + /// + /// This is only needed during host initialization before the first frame. + pub(crate) fn export_session(self) -> OutboxSession { + self.pool.export() + } + + /// Access scoped subscription APIs bound to the selected account. + pub fn scoped_subs<'o>(&'o mut self, accounts: &'o Accounts) -> ScopedSubApi<'o, 'a> { + self.scoped_sub_state.api(&mut self.pool, accounts) + } + + /// Access one-shot read APIs bound to the selected account. + pub fn oneshot<'o>(&'o mut self, accounts: &'o Accounts) -> OneshotApi<'o, 'a> { + OneshotApi::new(&mut self.pool, accounts) + } + + /// Access publishing APIs bound to the selected account. + pub fn publisher<'o>(&'o mut self, accounts: &'o Accounts) -> PublishApi<'o, 'a> { + PublishApi::new(&mut self.pool, accounts) + } + + /// Access explicit-relay publishing APIs (no account dependency). + pub fn publisher_explicit<'o>(&'o mut self) -> ExplicitPublishApi<'o, 'a> { + ExplicitPublishApi::new(&mut self.pool) + } + + /// Host-only relay ingestion + keepalive maintenance. + pub(crate) fn process_events(&mut self, ctx: &Context, ndb: &Ndb) { + try_process_events(ctx, &mut self.pool, ndb); + } + + /// Access read-only relay inspection data for UI rendering. + pub fn relay_inspect(&self) -> RelayInspectApi<'_, 'a> { + RelayInspectApi::new(&self.pool) + } + + /// Host account-switch transition hook for scoped subscription teardown/restore. + pub(crate) fn on_account_switched( + &mut self, + old_account: Pubkey, + new_account: Pubkey, + accounts: &Accounts, + ) { + self.scoped_sub_state.runtime_mut().on_account_switched( + &mut self.pool, + old_account, + new_account, + accounts, + ); + } + + /// Host/account hook to retarget selected-account-read scoped subscriptions. + /// + /// This retargets all live scoped subscriptions that resolve relays from + /// [`crate::RelaySelection::AccountsRead`] without requiring callers to + /// individually `set_sub(...)` every declaration. + pub(crate) fn retarget_selected_account_read_relays(&mut self, accounts: &Accounts) { + self.scoped_sub_state + .runtime_mut() + .retarget_selected_account_read_relays(&mut self.pool, accounts); + } +} + +#[profiling::function] +pub fn try_process_events(ctx: &Context, pool: &mut Outbox, ndb: &Ndb) { + let ctx2 = ctx.clone(); + let wakeup = move || { + ctx2.request_repaint(); + }; + + pool.outbox.keepalive_ping(wakeup); + + pool.outbox.try_recv(10, |ev| { + let from_client = match ev.relay_type { + RelayImplType::Websocket => false, + enostr::RelayImplType::Multicast => true, + }; + + { + profiling::scope!("ndb process event"); + if let Err(err) = ndb.process_event_with( + ev.event_json, + nostrdb::IngestMetadata::new() + .client(from_client) + .relay(ev.url), + ) { + tracing::error!("error processing event {}: {err}", ev.event_json); + } + } + }); +}