Skip to content

feat(chat): replace 2s chat poll with shared-key subscription router#94

Open
arkanoider wants to merge 3 commits into
mainfrom
feat/chat-subscription-router
Open

feat(chat): replace 2s chat poll with shared-key subscription router#94
arkanoider wants to merge 3 commits into
mainfrom
feat/chat-subscription-router

Conversation

@arkanoider

@arkanoider arkanoider commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Introduce a single long-lived chat subscription router (listen_for_chat_messages) that maintains one batched kind:1059 #p filter over all active shared-key pubkeys and routes incoming gift wraps by p tag, mirroring Mostro Mobile's SubscriptionManager. Replaces the 2-second admin_chat_interval poll (spawn_admin_chat_fetch / spawn_user_order_chat_fetch / CHAT_MESSAGES_SEMAPHORE).

  • Track/untrack via global ChatRouterCmd channel; startup track set (option B) via track_startup_chats (active orders + success, InProgress disputes).

  • Dynamic hooks: track after order upsert and on dispute take; untrack on terminal status, pre-active drop, and book-republish revert.

  • Respawn chat router after reconnect and key/fetch reloads. Chat kept live after success; on-disk transcripts preserve untracked terminal history.

Summary by CodeRabbit

  • New Features
    • Chat updates now stream live via a shared-key subscription router instead of periodic refreshes.
    • Startup, reconnect, and runtime reloads restore and continue chat history seamlessly.
    • Admin dispute chats start tracking immediately when disputes enter progress.
  • Bug Fixes
    • Improved continuity by re-subscribing after reconnect/reloads and hydrating history once per key.
    • More accurate subscription cleanup when orders reach terminal states, reducing duplicates/missed updates.
  • Documentation
    • Updated chat flow and startup behavior descriptions to match the new live model.

Introduce a single long-lived chat subscription router (listen_for_chat_messages) that maintains one batched kind:1059 #p filter over all active shared-key pubkeys and routes incoming gift wraps by p tag, mirroring Mostro Mobile's SubscriptionManager. Replaces the 2-second admin_chat_interval poll (spawn_admin_chat_fetch / spawn_user_order_chat_fetch / CHAT_MESSAGES_SEMAPHORE).

- Track/untrack via global ChatRouterCmd channel; startup track set (option B) via track_startup_chats (active orders + success, InProgress disputes).

- Dynamic hooks: track after order upsert and on dispute take; untrack on terminal status, pre-active drop, and book-republish revert.

- Respawn chat router after reconnect and key/fetch reloads. Chat kept live after success; on-disk transcripts preserve untracked terminal history.
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@arkanoider, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 54 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 279e9d00-5432-44d4-984f-356af76fdc36

📥 Commits

Reviewing files that changed from the base of the PR and between 0d3544f and 737e98c.

📒 Files selected for processing (2)
  • docs/MESSAGE_FLOW_AND_PROTOCOL.md
  • src/util/dm_utils/mod.rs

Walkthrough

Replaces periodic polling-based admin/user chat fetch with a persistent shared-key subscription router that batches relay subscriptions, hydrates history once per key, routes live gift-wraps by p tag, and wires startup, reconnect/reload respawn, DM/dispute tracking, and docs to the new flow.

Changes

Shared-key chat subscription router

Layer / File(s) Summary
Core chat_listener router module
src/util/chat_listener.rs, src/util/mod.rs
New module defines ChatKeyId and ChatRouterCmd, track/untrack APIs, history hydration, subscription rebuild, the router loop, and unit tests; it is re-exported publicly.
Startup chat tracking helper
src/ui/helpers/startup.rs, src/ui/helpers/mod.rs
Adds track_startup_chats, which enumerates active orders and in-progress disputes, resolves shared keys and cursors, and enqueues track commands.
Startup spawn wiring
src/startup.rs
Extends PostTerminalStartupInput and StartupBootstrap with chat update channels and chat_listener_handle, and spawns the router after initial tracking commands.
main.rs runtime lifecycle
src/main.rs
Wires router channels, aborts chat_listener_handle on shutdown paths, removes periodic chat polling, and respawns the listener on reconnect and after runtime reloads.
Respawn helper and channels
src/ui/key_handler/async_tasks.rs, src/ui/key_handler/mod.rs
Adds respawn_chat_listener and extends AppChannels / create_app_channels with chat_router_cmd_tx and chat_router_cmd_rx.
DM and dispute integration
src/util/dm_utils/mod.rs, src/util/order_utils/execute_take_dispute.rs, src/util/order_utils/execute_finalize_dispute.rs
Calls track and untrack helpers at order lifecycle transitions and dispute acceptance/finalization.
Removal of old polling functions
src/util/order_utils/fetch_scheduler.rs, src/util/order_utils/mod.rs
Deletes CHAT_MESSAGES_SEMAPHORE, spawn_admin_chat_fetch, spawn_user_order_chat_fetch, and their re-exports.
Documentation updates
docs/ADMIN_DISPUTES.md, docs/MESSAGE_FLOW_AND_PROTOCOL.md, docs/STARTUP_AND_CONFIG.md, docs/TUI_INTERFACE.md
Describes the new subscription router model replacing periodic polling for admin and user order chats.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • MostroP2P/mostrix#27: Modifies the same admin-chat polling path this PR removes and replaces with the subscription router.
  • MostroP2P/mostrix#42: Touches the same DM order-chat handling paths that now call maybe_track_order_chat and untrack_order_chat.
  • MostroP2P/mostrix#52: Shares the startup helper layer that now includes track_startup_chats.

Suggested reviewers: mostronatorcoder, grunch

Poem

A rabbit hopped from poll to push,
No more ticking clocks to rush.
One router hums with keys aglow,
And gift-wrap chats now stream and flow.
Track, untrack, and resubscribe—
Hoppy packets keep chats alive 🐇

🚥 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 title accurately summarizes the main change: replacing 2-second chat polling with a shared-key subscription router.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/chat-subscription-router

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/util/chat_listener.rs`:
- Around line 209-226: The chat subscription swap in chat_listener::subscribe
flow drops the active subscription too early by calling current_sub.take() and
unsubscribe before client.subscribe succeeds. Keep the existing subscription
alive until the new subscribe call in the match on client.subscribe(filter,
None).await returns Ok, then replace current_sub with the new subscription ID
and unsubscribe the old one only after that succeeds. If subscribe fails, leave
current_sub unchanged so live chat remains active.
- Around line 273-287: The chat listener currently hydrates history before the
new key is added to targets and before resubscribing, which creates a gap where
messages can be missed. In chat_listener.rs, update the flow around the one-shot
history hydration and resubscribe logic so the target is inserted into targets
and the live subscription is rebuilt before fetching/processing history, using
the existing emit_messages, fetch_gift_wraps_for_shared_key, and resubscribe
paths. Keep the history cutoff filtering intact, but ensure the subscription is
active first so any messages published during hydration are covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9322f2ac-ab5b-4d9e-aac1-ecc325216293

📥 Commits

Reviewing files that changed from the base of the PR and between c314469 and 2ee2077.

📒 Files selected for processing (16)
  • docs/ADMIN_DISPUTES.md
  • docs/MESSAGE_FLOW_AND_PROTOCOL.md
  • docs/STARTUP_AND_CONFIG.md
  • docs/TUI_INTERFACE.md
  • src/main.rs
  • src/startup.rs
  • src/ui/helpers/mod.rs
  • src/ui/helpers/startup.rs
  • src/ui/key_handler/async_tasks.rs
  • src/ui/key_handler/mod.rs
  • src/util/chat_listener.rs
  • src/util/dm_utils/mod.rs
  • src/util/mod.rs
  • src/util/order_utils/execute_take_dispute.rs
  • src/util/order_utils/fetch_scheduler.rs
  • src/util/order_utils/mod.rs
💤 Files with no reviewable changes (1)
  • src/util/order_utils/fetch_scheduler.rs

Comment thread src/util/chat_listener.rs
Comment on lines +209 to +226
if let Some(id) = current_sub.take() {
client.unsubscribe(&id).await;
}
if targets.is_empty() {
return;
}
let pubkeys: Vec<PublicKey> = targets.keys().copied().collect();
let filter = Filter::new().kind(Kind::GiftWrap).pubkeys(pubkeys).limit(0);
match client.subscribe(filter, None).await {
Ok(output) => {
log::debug!(
"[chat_live] subscribed to {} shared-key chat(s) subscription_id={}",
targets.len(),
output.val
);
*current_sub = Some(output.val);
}
Err(e) => log::warn!("[chat_live] failed to subscribe shared-key chats: {e}"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep the old chat subscription until the replacement succeeds.

Line 209 unsubscribes the current live subscription before client.subscribe succeeds. If the new subscribe fails, targets still contains chats but no live subscription remains until another command or respawn.

Proposed fix
 async fn resubscribe(
     client: &Client,
     targets: &HashMap<PublicKey, ChatTarget>,
     current_sub: &mut Option<SubscriptionId>,
 ) {
-    if let Some(id) = current_sub.take() {
-        client.unsubscribe(&id).await;
-    }
     if targets.is_empty() {
+        if let Some(id) = current_sub.take() {
+            client.unsubscribe(&id).await;
+        }
         return;
     }
     let pubkeys: Vec<PublicKey> = targets.keys().copied().collect();
     let filter = Filter::new().kind(Kind::GiftWrap).pubkeys(pubkeys).limit(0);
     match client.subscribe(filter, None).await {
@@
-            *current_sub = Some(output.val);
+            let previous_sub = current_sub.replace(output.val);
+            if let Some(id) = previous_sub {
+                client.unsubscribe(&id).await;
+            }
         }
         Err(e) => log::warn!("[chat_live] failed to subscribe shared-key chats: {e}"),
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Some(id) = current_sub.take() {
client.unsubscribe(&id).await;
}
if targets.is_empty() {
return;
}
let pubkeys: Vec<PublicKey> = targets.keys().copied().collect();
let filter = Filter::new().kind(Kind::GiftWrap).pubkeys(pubkeys).limit(0);
match client.subscribe(filter, None).await {
Ok(output) => {
log::debug!(
"[chat_live] subscribed to {} shared-key chat(s) subscription_id={}",
targets.len(),
output.val
);
*current_sub = Some(output.val);
}
Err(e) => log::warn!("[chat_live] failed to subscribe shared-key chats: {e}"),
if targets.is_empty() {
if let Some(id) = current_sub.take() {
client.unsubscribe(&id).await;
}
return;
}
let pubkeys: Vec<PublicKey> = targets.keys().copied().collect();
let filter = Filter::new().kind(Kind::GiftWrap).pubkeys(pubkeys).limit(0);
match client.subscribe(filter, None).await {
Ok(output) => {
log::debug!(
"[chat_live] subscribed to {} shared-key chat(s) subscription_id={}",
targets.len(),
output.val
);
let previous_sub = current_sub.replace(output.val);
if let Some(id) = previous_sub {
client.unsubscribe(&id).await;
}
}
Err(e) => log::warn!("[chat_live] failed to subscribe shared-key chats: {e}"),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/util/chat_listener.rs` around lines 209 - 226, The chat subscription swap
in chat_listener::subscribe flow drops the active subscription too early by
calling current_sub.take() and unsubscribe before client.subscribe succeeds.
Keep the existing subscription alive until the new subscribe call in the match
on client.subscribe(filter, None).await returns Ok, then replace current_sub
with the new subscription ID and unsubscribe the old one only after that
succeeds. If subscribe fails, leave current_sub unchanged so live chat remains
active.

Comment thread src/util/chat_listener.rs
Comment on lines +273 to +287
// One-shot history hydration (relay subscriptions alone don't replay history).
match fetch_gift_wraps_for_shared_key(&client, &shared_keys).await {
Ok(messages) => {
let cutoff = since.unwrap_or(0);
let history: Vec<(String, i64, PublicKey)> = messages
.into_iter()
.filter(|(_, ts, _)| *ts >= cutoff)
.collect();
emit_messages(&target, history, &admin_chat_updates_tx, &user_order_chat_updates_tx);
}
Err(e) => log::warn!("[chat_live] history fetch failed for {key_id:?}: {e}"),
}

targets.insert(target_pubkey, target);
resubscribe(&client, &targets, &mut current_sub).await;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Subscribe the key before history hydration.

The router fetches history before adding the key to targets and rebuilding the live subscription. Any message published after the fetch snapshot but before Line 287 subscribes can be missed.

Proposed fix
                         let target = ChatTarget {
                             key_id: key_id.clone(),
                             shared_keys: shared_keys.clone(),
                             local_trade_pubkey,
                         };
 
+                        targets.insert(target_pubkey, target);
+                        resubscribe(&client, &targets, &mut current_sub).await;
+
                         // One-shot history hydration (relay subscriptions alone don't replay history).
                         match fetch_gift_wraps_for_shared_key(&client, &shared_keys).await {
                             Ok(messages) => {
                                 let cutoff = since.unwrap_or(0);
                                 let history: Vec<(String, i64, PublicKey)> = messages
                                     .into_iter()
                                     .filter(|(_, ts, _)| *ts >= cutoff)
                                     .collect();
-                                emit_messages(&target, history, &admin_chat_updates_tx, &user_order_chat_updates_tx);
+                                if let Some(target) = targets.get(&target_pubkey) {
+                                    emit_messages(target, history, &admin_chat_updates_tx, &user_order_chat_updates_tx);
+                                }
                             }
                             Err(e) => log::warn!("[chat_live] history fetch failed for {key_id:?}: {e}"),
                         }
-
-                        targets.insert(target_pubkey, target);
-                        resubscribe(&client, &targets, &mut current_sub).await;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// One-shot history hydration (relay subscriptions alone don't replay history).
match fetch_gift_wraps_for_shared_key(&client, &shared_keys).await {
Ok(messages) => {
let cutoff = since.unwrap_or(0);
let history: Vec<(String, i64, PublicKey)> = messages
.into_iter()
.filter(|(_, ts, _)| *ts >= cutoff)
.collect();
emit_messages(&target, history, &admin_chat_updates_tx, &user_order_chat_updates_tx);
}
Err(e) => log::warn!("[chat_live] history fetch failed for {key_id:?}: {e}"),
}
targets.insert(target_pubkey, target);
resubscribe(&client, &targets, &mut current_sub).await;
let target = ChatTarget {
key_id: key_id.clone(),
shared_keys: shared_keys.clone(),
local_trade_pubkey,
};
targets.insert(target_pubkey, target);
resubscribe(&client, &targets, &mut current_sub).await;
// One-shot history hydration (relay subscriptions alone don't replay history).
match fetch_gift_wraps_for_shared_key(&client, &shared_keys).await {
Ok(messages) => {
let cutoff = since.unwrap_or(0);
let history: Vec<(String, i64, PublicKey)> = messages
.into_iter()
.filter(|(_, ts, _)| *ts >= cutoff)
.collect();
if let Some(target) = targets.get(&target_pubkey) {
emit_messages(target, history, &admin_chat_updates_tx, &user_order_chat_updates_tx);
}
}
Err(e) => log::warn!("[chat_live] history fetch failed for {key_id:?}: {e}"),
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/util/chat_listener.rs` around lines 273 - 287, The chat listener
currently hydrates history before the new key is added to targets and before
resubscribing, which creates a gap where messages can be missed. In
chat_listener.rs, update the flow around the one-shot history hydration and
resubscribe logic so the target is inserted into targets and the live
subscription is rebuilt before fetching/processing history, using the existing
emit_messages, fetch_gift_wraps_for_shared_key, and resubscribe paths. Keep the
history cutoff filtering intact, but ensure the subscription is active first so
any messages published during hydration are covered.

arkanoider and others added 2 commits July 7, 2026 21:49
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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.

1 participant