close
Skip to content

fix(desktop): kill WebSocket flood and fix Markdown <p><div> nesting#368

Merged
wesbillman merged 4 commits intomainfrom
fix/desktop-relay-flood-and-markdown-nesting
Apr 20, 2026
Merged

fix(desktop): kill WebSocket flood and fix Markdown <p><div> nesting#368
wesbillman merged 4 commits intomainfrom
fix/desktop-relay-flood-and-markdown-nesting

Conversation

@wesbillman
Copy link
Copy Markdown
Collaborator

@wesbillman wesbillman commented Apr 20, 2026

Summary

Two desktop bugfixes from the same debugging session, with an adversarial-review follow-up that caught regressions in the original approach.

  • Relay flood → unresponsive app: Home-feed mention subscription was tearing down + resubscribing all N channels (~100 for heavy users) on every channels-query refetch, firing hundreds of plugin:websocket|send IPC calls per second and saturating the Tauri WebView. Fixed with a diff-based subscription manager: a ref-backed Map<channelId, dispose> that only subs/unsubs the delta. Channels refetch with identical IDs does zero network work.
  • Markdown <p><div> hydration error: paragraphs containing an image + text/link rendered as <p> wrapping a <div> because the renderer only switched to <div> when the paragraph had zero non-image children. Now switches whenever any image is present.

Details

Relay flood fix (src/features/channels/useLiveChannelUpdates.ts, src/shared/api/relayClientSession.ts)

Diff-based mention sub manager:

  • mentionSubsRef: Map<channelId, dispose> persists across renders.
  • On sync: remove subs whose channel left the target set; subscribe only channels that are new.
  • Pubkey change triggers full tear-down + resubscribe (rare).
  • Per-channel subscribe failures retry with exponential backoff (1s → 30s capped).
  • Dispose all on unmount.

Why not a single global sub: an earlier attempt dropped the #h filter for one global #p: [pubkey] subscription. Codex review and CI caught that the relay intentionally does not fan out channel-scoped events to global subs (sprout-relay/src/subscription.rs:136) — live mentions stopped arriving entirely and the live forum mentions refetch the home feed integration test failed. Reverted.

Also:

  • Global stream subscription effect now deps on hasLiveChannels (boolean) so channels refetches with identical IDs don't cycle it.

Markdown fix (src/shared/ui/markdown.tsx, src/shared/ui/markdownUtils.ts)

  • hasBlockMedia now returns true whenever any image child is present.
  • The <div> fallback carries paragraphClassName so mixed text+media paragraphs keep their line-height.

Commits

  1. fix(desktop): render Markdown paragraphs with media as <div>…
  2. fix(desktop): collapse per-channel mention subs into one global subscription (original attempt — superseded by feat: add desktop app #3)
  3. fix(desktop): use diff-based per-channel mention sub manager

Test plan

  • pnpm check, pnpm typecheck, all lefthook pre-push checks (desktop-build, rust-clippy, rust-tests, mobile-test, etc.) green
  • Codex adversarial review of current diff
  • CI integration suite — specifically live forum mentions refetch the home feed without waiting for polling (previously failing)
  • Reviewer sanity-check: open a channel containing a mixed image+text message with a video URL — no <p><div> hydration warning in DevTools console
  • Reviewer sanity-check: with multiple channels and active bots, observe plugin:websocket|send traffic remains low in steady state

🤖 Generated with Claude Code

wesbillman and others added 4 commits April 20, 2026 09:36
… <p><div> nesting

The img renderer always emits block-level markup (lightbox div or video
wrapper), so any paragraph containing an image produced an invalid
<p><div> tree when the paragraph also had text or links. Switch the
paragraph renderer to <div> whenever any image child is present, and
preserve paragraph line-height styling on the fallback.

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

The Home-feed mention subscription fanned out one REQ per channel
(~100 for heavy users) and tore down + resubscribed whenever the
channels list refetched, resulting in hundreds of plugin:websocket|send
IPC calls per second and an unresponsive WebView. Replace the fan-out
with a single subscription filtered by #p: [currentPubkey] so the relay
does the cross-channel matching.

Also:
- Reset reconnectDelayMs only after replayLiveSubscriptions succeeds,
  so a replay crash-loop properly backs off instead of retrying at 1s.
- Back the global stream subscription effect on a boolean
  (hasLiveChannels) so channels-query refetches with identical IDs no
  longer churn the subscription.
- Exponential backoff (1s → 30s) on mention-subscription retry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex adversarial review caught two regressions from the previous
relay-flood commit:

1. Removing #h turned subscribeToMentionsForPubkey into a global
   subscription, and the relay intentionally does not fan out
   channel-scoped events to global subs (sprout-relay
   subscription.rs:136). New live mentions silently stopped arriving,
   breaking the "live forum mentions refetch home feed" integration
   test. Restore subscribeToChannelMentionEvents with #h.
2. Resetting reconnectDelayMs after replayLiveSubscriptions meant a
   single transient post-auth replay failure could inherit up to a 30s
   backoff instead of recovering in 1s. Restore the reset-before-replay
   position.

Replace the per-channel fan-out tear-down+resubscribe pattern with a
diff-based subscription manager: a ref-backed Map<channelId, dispose>
means channels refetches with identical IDs do zero network work, and a
legitimate channel add/remove only subs/unsubs the delta rather than
cycling all N subs. Per-channel subscribe failures retry with
exponential backoff (1s → 30s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Integration tests "live mentions refetch the home feed" and
"live forum mentions refetch the home feed" were failing because the
onEvent callback for each mention sub was wrapped in an isCancelled
check that closed over a local variable from the effect run that
created the sub. When the effect re-ran and the sub persisted in the
map (the common case — channels unchanged), the original callback was
retained, but its isCancelled flag had been flipped to true by the
prior effect's cleanup, so events on the long-lived sub were silently
dropped.

Pass handleMentionEvent directly. It is a useEffectEvent and always
captures the latest state; no per-run cancellation wrapper is needed.
The outer isCancelled check after subscribe resolves is still required
to dispose in-flight subscribes whose effect was torn down.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@wesbillman wesbillman merged commit eda85c8 into main Apr 20, 2026
10 checks passed
@wesbillman wesbillman deleted the fix/desktop-relay-flood-and-markdown-nesting branch April 20, 2026 17:53
fsola-sq added a commit that referenced this pull request Apr 20, 2026
…-binding

* origin/main:
  fix(desktop): eliminate agent startup beachball (#374)
  fix(desktop): resolve agent command path for DMG builds (#372)
  fix(desktop): remove stale sprout-admin prereq, add sidecar tooling (#371)
  Add server cross-compile and macOS desktop build CI jobs (#369)
  Fix forum post card bugs on desktop and mobile (#370)
  fix(desktop): kill WebSocket flood and fix Markdown <p><div> nesting (#368)
  perf: caching, batched DM resolution, bounded audit, global kind index (#367)
  fix: staging to generate stubs as needed (#366)
  chore(deps): update rust crate axum to v0.8.9 (#365)
  chore(deps): update dependency @tanstack/react-router to v1.168.22 (#364)
  feat(desktop): autoscroll thread sidebar for new replies (#363)
  fix(desktop): eliminate 10+ second UI freeze on startup (#361)
  feat(desktop): bundle sprout-acp and sprout-mcp-server as Tauri sidecars (#362)
  Remove release pipeline from public repo (#360)

Amp-Thread-ID: https://ampcode.com/threads/T-019dab7a-5979-7401-83a1-509b9adfe4a0
Co-authored-by: Amp <amp@ampcode.com>

# Conflicts:
#	crates/sprout-relay/src/state.rs
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