Skip to content

Fix startup chat stacking and defer init work#425

Merged
benthecarman merged 4 commits intosledtools:masterfrom
benthecarman:slow-startup
Mar 4, 2026
Merged

Fix startup chat stacking and defer init work#425
benthecarman merged 4 commits intosledtools:masterfrom
benthecarman:slow-startup

Conversation

@benthecarman
Copy link
Copy Markdown
Collaborator

@benthecarman benthecarman commented Mar 4, 2026

Summary

  • Prevent multiple chat screens stacking when taps queue during actor initialization — Rust clears existing Chat and GroupInfo screens before pushing a new one
  • Defer non-critical session init work (profile sync, follow list, key packages, subscriptions) via internal events so queued user actions are processed first
  • Same deferral for foreground transitions — only reopen_mdk() runs synchronously, heavy refresh is deferred

Test plan

  • Cold launch with stored credentials → rapidly tap chats during loading → only one chat opens
  • Return from background → rapidly tap chats → only one chat opens, no stacking
  • Normal chat navigation still works (tap chat, swipe back, tap another)
  • Follow list, profile sync, subscriptions still complete after deferred init
  • cargo test passes (225 unit + 31 integration)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Prevented duplicate or orphaned Chat/Group screens from stacking during navigation.
  • Performance Improvements

    • Deferred non-critical session and foreground work so user actions run immediately, improving responsiveness and startup/foreground perceived speed.
  • Tests

    • Added unit tests covering chat-screen navigation behaviors and deferred init/foreground handling.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

Defers heavy post-login and foreground refresh work into two new internal events (CompleteSessionInit, RefreshAfterForeground), adds coalescing flags for those deferred events, and updates chat navigation to prune stacked Chat/GroupInfo screens before pushing a Chat.

Changes

Cohort / File(s) Summary
Core Event Handling & Chat Management
rust/src/core/mod.rs
Added two private coalescing flags (deferred_foreground_pending, deferred_session_init_pending), handlers for InternalEvent::CompleteSessionInit and InternalEvent::RefreshAfterForeground that run deferred init/refresh steps and clear flags; open_chat_screen now prunes orphaned Screen::Chat/Screen::GroupInfo entries to avoid duplicates.
Session Initialization
rust/src/core/session.rs
Reworked post-login flow to set deferred_session_init_pending = true and post CoreMsg::Internal(CompleteSessionInit) instead of performing init inline; made hydrate_follow_list_from_cache pub(super).
Event Type Definitions
rust/src/updates.rs
Added two new unit variants to InternalEvent: CompleteSessionInit and RefreshAfterForeground.
Action Changes
rust/src/core/...
AppAction::Foregrounded now defers heavy refresh by posting InternalEvent::RefreshAfterForeground with coalescing logic instead of executing refresh inline.
Tests
rust/src/core/...tests
Added unit tests covering chat navigation behaviors (replacement, GroupInfo clearance, deduplication) and no-op behavior of CompleteSessionInit / RefreshAfterForeground when logged out.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant UI as "UI / Navigation"
  participant Core as "AppCore"
  participant Storage as "Local Storage"
  participant Network as "Network / Profile API"
  participant Push as "Push Service"

  UI->>Core: Foregrounded (AppAction::Foregrounded)
  Core->>Core: post InternalEvent::RefreshAfterForeground
  Note over Core,Storage: when processed (if logged in & not coalesced)
  Core->>Storage: refresh_from_storage
  Core->>Network: refresh_my_profile
  Core->>Network: refresh_follow_list

  UI->>Core: start_session_with_signer (login)
  Core->>Core: set deferred_session_init_pending + post InternalEvent::CompleteSessionInit
  Note over Core,Storage: when processed (if logged in & not coalesced)
  Core->>Storage: cache_missing_profile_pics
  Core->>Network: refresh_my_profile
  Core->>Core: hydrate_follow_list_from_cache
  Core->>Network: refresh_follow_list
  Core->>Network: publish_subscriptions
  Core->>Push: register_push_device
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I sat and waited, paws tucked tight,
Deferred the bustle for the quiet night.
I pruned the chats, avoided the mess,
Let init wake gently — no frantic stress. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 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 changes: fixing chat screen stacking issues and deferring initialization work to improve startup performance.

✏️ 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

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

devin-ai-integration[bot]

This comment was marked as resolved.

benthecarman and others added 2 commits March 4, 2026 14:37
During session restoration the Rust actor is busy with blocking init
work while the chat list is already visible. Taps dispatch OpenChat
actions that queue up and all fire when the actor finishes, stacking
multiple chat screens. Fix both sides:

- Rust: clear existing Chat screens from the nav stack before pushing
- iOS: debounce chat list taps so only the first fires until navigation
  completes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
During session restore and foreground transitions, the Rust actor was
blocked for the entire duration of initialization, preventing any
queued user actions (like chat taps) from being processed.

Split the work: critical path (auth + chat list) runs synchronously,
then remaining work (profile sync, follow list, key packages,
subscriptions) is deferred via internal events so pending user actions
in the channel are processed first.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (2)
rust/src/core/session.rs (1)

88-92: Attach session identity to deferred init event.

On Line 90–92, CompleteSessionInit is queued without session context. If the user session changes before processing, stale deferred work can run against the current session. Consider sending a session discriminator (e.g., pubkey/revision token) and validating it in the event handler.

Proposed direction
- let _ = self.core_sender.send(CoreMsg::Internal(Box::new(
-     InternalEvent::CompleteSessionInit,
- )));
+ let _ = self.core_sender.send(CoreMsg::Internal(Box::new(
+     InternalEvent::CompleteSessionInit {
+         session_pubkey_hex: pubkey_hex.clone(),
+     },
+ )));
- CompleteSessionInit,
+ CompleteSessionInit { session_pubkey_hex: String },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/core/session.rs` around lines 88 - 92, The deferred
CompleteSessionInit event is sent without any session context (see
CoreMsg::Internal and InternalEvent::CompleteSessionInit), risking stale work if
the user session changes before the message is handled; update the InternalEvent
enum to include a session discriminator (e.g., session_pubkey or revision
token), change the send site
(core_sender.send(CoreMsg::Internal(Box::new(InternalEvent::CompleteSessionInit,
session_token)))) to attach the current session identifier, and in the handler
for InternalEvent::CompleteSessionInit validate the attached token against the
current session before applying any init work (return/skip if it does not
match).
rust/src/core/mod.rs (1)

4578-4582: Consider coalescing repeated foreground refresh events.

Each Foregrounded dispatch enqueues RefreshAfterForeground; rapid foreground bursts can trigger redundant refresh_all_from_storage/profile/follow refresh cycles. A small pending flag would collapse duplicates.

♻️ Suggested coalescing pattern
 struct AppCore {
+    refresh_after_foreground_pending: bool,
     ...
 }

 // in AppCore::new initializer
+refresh_after_foreground_pending: false,

 // AppAction::Foregrounded branch
-                    let _ = self.core_sender.send(CoreMsg::Internal(Box::new(
-                        InternalEvent::RefreshAfterForeground,
-                    )));
+                    if !self.refresh_after_foreground_pending {
+                        self.refresh_after_foreground_pending = true;
+                        let _ = self.core_sender.send(CoreMsg::Internal(Box::new(
+                            InternalEvent::RefreshAfterForeground,
+                        )));
+                    }

 // InternalEvent::RefreshAfterForeground handler
             InternalEvent::RefreshAfterForeground => {
+                self.refresh_after_foreground_pending = false;
                 if !self.is_logged_in() {
                     return;
                 }
                 self.refresh_all_from_storage();
                 self.refresh_my_profile(false);
                 self.refresh_follow_list();
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/core/mod.rs` around lines 4578 - 4582, Repeated Foregrounded
dispatches enqueue duplicate InternalEvent::RefreshAfterForeground messages
causing redundant refresh_all_from_storage/profile/follow refresh cycles; add a
small pending flag on the actor (e.g., a boolean field like
refresh_after_foreground_pending or an AtomicBool) and change the code that
sends CoreMsg::Internal(Box::new(InternalEvent::RefreshAfterForeground)) to
first check-and-set that flag so it only sends when not already pending, and
ensure the flag is cleared when the consumer handles
InternalEvent::RefreshAfterForeground (i.e., at the start or end of the
refresh_all_from_storage/profile/follow refresh handling) so future foreground
events can re-enqueue.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@rust/src/core/mod.rs`:
- Around line 4578-4582: Repeated Foregrounded dispatches enqueue duplicate
InternalEvent::RefreshAfterForeground messages causing redundant
refresh_all_from_storage/profile/follow refresh cycles; add a small pending flag
on the actor (e.g., a boolean field like refresh_after_foreground_pending or an
AtomicBool) and change the code that sends
CoreMsg::Internal(Box::new(InternalEvent::RefreshAfterForeground)) to first
check-and-set that flag so it only sends when not already pending, and ensure
the flag is cleared when the consumer handles
InternalEvent::RefreshAfterForeground (i.e., at the start or end of the
refresh_all_from_storage/profile/follow refresh handling) so future foreground
events can re-enqueue.

In `@rust/src/core/session.rs`:
- Around line 88-92: The deferred CompleteSessionInit event is sent without any
session context (see CoreMsg::Internal and InternalEvent::CompleteSessionInit),
risking stale work if the user session changes before the message is handled;
update the InternalEvent enum to include a session discriminator (e.g.,
session_pubkey or revision token), change the send site
(core_sender.send(CoreMsg::Internal(Box::new(InternalEvent::CompleteSessionInit,
session_token)))) to attach the current session identifier, and in the handler
for InternalEvent::CompleteSessionInit validate the attached token against the
current session before applying any init work (return/skip if it does not
match).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8e8e5f26-3211-4fd3-a61c-4ab106b73950

📥 Commits

Reviewing files that changed from the base of the PR and between ef25de8 and 1b10ff2.

📒 Files selected for processing (3)
  • rust/src/core/mod.rs
  • rust/src/core/session.rs
  • rust/src/updates.rs

benthecarman and others added 2 commits March 4, 2026 14:46
Address review feedback: retain filter now also strips orphaned
GroupInfo screens when clearing stacked Chat screens. Add tests for
screen dedup and for the logged-out guard on the deferred init events.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rapid foreground bursts could enqueue multiple RefreshAfterForeground
events, each triggering a full refresh_all_from_storage cycle. Add
pending flags that collapse duplicates: the flag is set when enqueuing
and cleared when the handler runs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
rust/src/core/mod.rs (1)

4578-4582: Consider coalescing foreground refresh events.

Repeated foreground actions can queue multiple RefreshAfterForeground jobs and redo the same heavy refresh work. A simple in-flight/pending flag (similar to your subscription recompute pattern) would avoid redundant cycles.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/core/mod.rs` around lines 4578 - 4582, The foreground refresh
scheduling currently always enqueues
CoreMsg::Internal(InternalEvent::RefreshAfterForeground) which can result in
many redundant refresh jobs; add a boolean flag on the actor state (e.g.,
refresh_in_flight or pending_refresh) and check it before calling
self.core_sender.send in the site where RefreshAfterForeground is enqueued, set
the flag when you enqueue it, and clear the flag in the handler that processes
InternalEvent::RefreshAfterForeground once the heavy refresh completes
(mirroring your subscription recompute pattern) so duplicate refreshes are
coalesced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@rust/src/core/mod.rs`:
- Around line 4578-4582: The foreground refresh scheduling currently always
enqueues CoreMsg::Internal(InternalEvent::RefreshAfterForeground) which can
result in many redundant refresh jobs; add a boolean flag on the actor state
(e.g., refresh_in_flight or pending_refresh) and check it before calling
self.core_sender.send in the site where RefreshAfterForeground is enqueued, set
the flag when you enqueue it, and clear the flag in the handler that processes
InternalEvent::RefreshAfterForeground once the heavy refresh completes
(mirroring your subscription recompute pattern) so duplicate refreshes are
coalesced.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4ccd28be-58d0-4a0c-9e6a-3f83fdf9283f

📥 Commits

Reviewing files that changed from the base of the PR and between 1b10ff2 and 2795b48.

📒 Files selected for processing (1)
  • rust/src/core/mod.rs

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
rust/src/core/mod.rs (1)

7304-7385: Consider adding one direct coalescing test for duplicate scheduling.

These tests cover route dedup and logged-out no-op well. A focused test that triggers repeated foreground/session-init scheduling and asserts collapse-to-one would lock in the coalescing contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/core/mod.rs` around lines 7304 - 7385, Add a focused test that
verifies duplicate scheduling of the same internal event is coalesced: create a
core via make_core, call
core.handle_internal(crate::updates::InternalEvent::RefreshAfterForeground) (or
::CompleteSessionInit) twice in a row, then assert only one instance of that
event is pending/scheduled (by inspecting the core state/queue that holds
pending internal events) or by advancing/processing scheduled work and asserting
the side-effect ran exactly once; reference core.handle_internal and the enum
variants InternalEvent::RefreshAfterForeground and
InternalEvent::CompleteSessionInit to locate where to add this test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@rust/src/core/mod.rs`:
- Around line 7304-7385: Add a focused test that verifies duplicate scheduling
of the same internal event is coalesced: create a core via make_core, call
core.handle_internal(crate::updates::InternalEvent::RefreshAfterForeground) (or
::CompleteSessionInit) twice in a row, then assert only one instance of that
event is pending/scheduled (by inspecting the core state/queue that holds
pending internal events) or by advancing/processing scheduled work and asserting
the side-effect ran exactly once; reference core.handle_internal and the enum
variants InternalEvent::RefreshAfterForeground and
InternalEvent::CompleteSessionInit to locate where to add this test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7c9148f8-635d-42c4-8163-1adc208223d5

📥 Commits

Reviewing files that changed from the base of the PR and between 2795b48 and 69d0b01.

📒 Files selected for processing (2)
  • rust/src/core/mod.rs
  • rust/src/core/session.rs

@benthecarman benthecarman merged commit e70972d into sledtools:master Mar 4, 2026
18 checks passed
@benthecarman benthecarman deleted the slow-startup branch March 4, 2026 20:58
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