Skip to content

Async client + UI refactor: eliminate polling, context-based state#2

Merged
intendednull merged 6 commits into
mainfrom
feat/async-client-ui-refactor
Mar 25, 2026
Merged

Async client + UI refactor: eliminate polling, context-based state#2
intendednull merged 6 commits into
mainfrom
feat/async-client-ui-refactor

Conversation

@intendednull
Copy link
Copy Markdown
Owner

@intendednull intendednull commented Mar 25, 2026

Summary

  • Replace std::sync::mpsc with futures::channel::mpsc in willow-client, eliminating the 16ms command poll timer (WASM) and enabling zero-latency async event delivery
  • Split monolithic Client struct into SharedState + ClientHandle (cloneable) + ClientEventLoop (async)
  • Replace the 50ms set_interval poll loop in the Leptos UI with spawn_local event processing
  • Introduce AppState context with grouped sub-structs (ChatState, NetworkState, ServerState, UiState, VoiceState), replacing 30 prop-drilled signals
  • Migrate 8 components from ClientHandle prop to use_context
  • Extract event_processing.rs and handlers.rs from the 800-line App monolith

Test plan

  • just check passes (fmt + clippy + test + WASM)
  • All 116 client tests + 1 doctest pass
  • Zero clippy warnings
  • WASM compilation check passes
  • Manual testing on https://willow.intendednull.com/ (deployed)
  • Browser E2E tests (just test-browser)

🤖 Generated with Claude Code

intendednull and others added 6 commits March 24, 2026 22:45
…nt crate

Eliminates the 16ms command polling timer in the WASM network loop.
Commands are now awaited directly via futures::select!.
Removes ClientNotification enum and profile_broadcast_counter
(to be replaced by async timers in ClientEventLoop later).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces SharedState (Rc<RefCell<>>) and ClientHandle (cloneable).
All 48+ public methods moved from Client to ClientHandle with &self
signatures (mutation through RefCell). Client struct removed.
ClientEventLoop stub defined for Task 3.

Key changes:
- SharedState holds all mutable state (ClientState, Identity, config, etc.)
- ClientHandle wraps Rc<RefCell<SharedState>> + cmd_tx + event_rx
- Methods returning borrowed data (peers(), active_voice_channel(),
  active_server_id()) now return owned types
- state()/state_mut() removed; roles_data() accessor added
- on_connected(), reconcile_topic_map(), init_event_state_for_server(),
  apply_event_shared(), emit_client_events_for() extracted as free
  functions taking explicit parameters
- All 116 tests pass, doc-test passes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move the synchronous poll() method from ClientHandle into an async
run() loop on ClientEventLoop. Network events are awaited via
futures::select!, drained into batches for efficiency, processed
through process_batch(), and forwarded as ClientEvents. Profile
re-broadcasts use real async timers (tokio on native, gloo-timers
on WASM) instead of tick counting. State verification after sync
is handled outside the batch borrow to avoid RefCell conflicts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Defines structured signal containers for context-based state management.
create_signals() produces all 30 signal pairs grouped into sub-structs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…d state

- Create event_processing.rs with process_event_batch() and refresh_all_signals()
- Create handlers.rs with action handler constructors (send, edit, delete,
  react, channel switch, server switch, pin)
- Rewrite App component: replace set_interval poll loop with spawn_local
  async event processing via ClientEventLoop
- Replace 30+ loose signal declarations with state::create_signals()
- Migrate 8 components from ClientHandle prop to use_context::<WebClientHandle>()
  (sidebar, member_list, welcome, add_server, server_settings, settings,
  file_share, roles)
- Add accessor methods to ClientHandle (server_owner, has_permission,
  current_channel, unread_counts) for component access without exposing
  SharedState internals
- Replace ClientHandle = SendWrapper<Rc<RefCell<Client>>> with
  WebClientHandle = SendWrapper<willow_client::ClientHandle>

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@intendednull intendednull merged commit cae0ea7 into main Mar 25, 2026
intendednull added a commit that referenced this pull request Apr 26, 2026
lifecycle, fix IrohBlobStore spec drift, track 4 new follow-ups

Round 2 review (two fresh agents) verified all 15 round-1 fixes
land cleanly with no regressions, then surfaced 8 new findings
(0 critical, 3 medium, 5 low) by widening scope to cross-component
interactions, perf, and API surface.

Fixed inline (trivial doc / spec):

- Add an "Actor coordination signal" row to the spec decision tree
  + CLAUDE.md table covering tokio::sync::watch / oneshot /
  broadcast / Notify, with the explicit rule that
  tokio::sync::Mutex is forbidden for business state on the same
  terms as std/parking_lot Mutex. Closes the spec gap that left
  contributors without guidance on async channels. (round-2 #3)
- Reconcile spec § 184 with the corrected IrohBlobStore comment
  (round-1 fixed the code, missed the spec). The blob store is
  not an iroh-callback boundary — it's an interim stub. The relay-
  status timestamp Mutex stays in the iroh boundary list. (round-2 #4)
- Document the web `_event_loop` drop pattern in `crates/web/src/app.rs`
  so future readers see explicitly that the actor System is process-
  scoped on web (page reload tears everything down) and that any
  actor needing pre-close cleanup must route via `beforeunload`,
  not Drop. (round-2 #8)

Tracked as new follow-ups in spec § Follow-up work:

- F5. SearchActor head-of-line + rebuild-storm fix. Rebuild blocks
  Query in FIFO order; the rebuild Effect has no debounce. Fix is
  chunked-Rebuild + Debounce<Rebuild> wrap. (round-2 #1, #2 — Med)
- F6. Browser-tier coverage for SearchIndexHandle consumers. The
  spawn_local + Effect path has no wasm-pack test. (round-2 #6)
- F7. Sealed ClientSpawner to narrow the system() API surface,
  rather than exposing the full SystemHandle. (round-2 #7)
- F8. Search-query debouncing-flicker fix via generation tag or
  Leptos Resource migration. (round-2 #5)

Each follow-up has a "Trigger:" line naming the dedicated PR title.

`just check` green: clippy zero warnings, 1003+ tests pass, WASM
compile clean. Loop terminates here per the user's two-round cap;
no Critical issues remain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.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