Skip to content

feat(desktop): auto-reload stale renderer when the user is away - #5588

Open
wpfleger96 wants to merge 1 commit into
mainfrom
hayt/idle-auto-reload
Open

feat(desktop): auto-reload stale renderer when the user is away#5588
wpfleger96 wants to merge 1 commit into
mainfrom
hayt/idle-auto-reload

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Aug 11, 2026

Copy link
Copy Markdown
Member

Summary

Long-lived Buzz desktop renderer sessions accumulate heap and timer churn until GC pauses queue behind input, degrading scroll and click latency (the multi-GB, multi-hour sessions that motivate this). The Cmd+R clean-teardown reload clears it without touching the native engine or its child agents — but relies on the user noticing the degradation and acting.

This adds an idle backstop that performs the same clean-teardown reload automatically, but only when the user is provably away and the session is old enough to have accumulated the churn — never during use.

How it decides

The backstop arms once the renderer session is ~12h old, then checks once a minute. It reloads only when all of these hold:

  • Session age >= 12h
  • Window unfocused
  • OS idle >= 30 min (unknown/unsupported idle API → never fire)
  • No volatile, reload-destroyed work outstanding

"Volatile work" is a single blocker covering everything a reload would lose:

  • A starting or active Huddle
  • A foreground or background media upload, including the isCompleting/onComplete settling window
  • Queued local File attachments a draft cannot serialize
  • Any in-flight send/mutation/native operation (queryClient.isMutating() — sends, media pickers, install/import/save all run as mutations)

Draft text survives a reload — persisted drafts sit in localStorage, and text typed since the last persist is flushed synchronously on pagehide (see below) — so draft text is deliberately not a blocker.

Reload primitive

Cmd+R and the backstop now share one idempotent requestRendererReload():

  • A module-level in-flight promise collapses overlapping triggers (a second Cmd+R, or the backstop firing mid-teardown) so the native WebSocket teardown never runs twice.
  • Teardown is bounded to RELOAD_TEARDOWN_TIMEOUT_MS (500 ms); a hung socket loses to the timeout.
  • A rejecting teardown is logged and swallowed, and the reload always runs in a finally — a rejection can no longer win the Promise.race and strand the renderer on a stale heap (the pre-existing Cmd+R bug this replaces).

Composer flush on reload

Live composer editor text was persisted to localStorage only on the draft-key-change effect cleanup in useDraftPersistSnapshot.ts, and location.reload() does not run React effect cleanup — so text typed-but-not-yet-persisted would be lost on any reload (backstop, Cmd+R, or window close). useDraftPersistLifecycle now registers a pagehide listener that flushes the current draft synchronously, mirroring the precedent in useObservedUnreadPersistence.ts. The persist logic is factored into one persistDraftSnapshot(key, channelId) helper so the key-change cleanup and the pagehide flush share a single write path — the cleanup persists the outgoing draft, the flush persists the current one, from the same live editor content. This closes the reload-destroyed-text gap for all three reload paths in one mechanism.

Structure

  • All fire/withhold logic is a pure, exhaustively tested predicate (idleAutoReloadPolicy.ts); the hook (useIdleAutoReload.ts) only wires live signals, arms after the session-age threshold, and serializes overlapping checks.
  • Every signal is read imperatively at check time (once a minute), so the hook holds no reactive subscription and never re-renders the app tree.
  • The controller mounts inside the ready main-window shell only, guarded by huddleWindowChannelId() === null — so Huddle companion windows and every onboarding/blocking/reset/keyring/relaunch screen are ineligible by construction, and isMutating() reads the message-send query client.

@wpfleger96
wpfleger96 requested a review from a team as a code owner August 11, 2026 18:16
Long-lived renderer sessions accumulate heap and timer churn until GC
pauses queue behind input, degrading scroll and click latency. The Cmd+R
clean-teardown reload clears it without touching the native engine or its
child agents, but relies on the user noticing and acting.

Add an idle backstop that performs the same reload automatically once a
session is ~12h old and the user is provably away (OS idle >= 30 min and
the window unfocused). It withholds the reload while any reload-destroyed
volatile work is outstanding: a starting/active huddle, a foreground or
background media upload including its completion settling window, queued
local file attachments a draft cannot serialize, or any in-flight
send/mutation/native operation. Persisted draft text survives via
localStorage, so it is deliberately not a blocker.

The reload primitive is one idempotent requestRendererReload() shared by
Cmd+R and the backstop: a module-level in-flight promise collapses
overlapping triggers so the native WebSocket teardown never runs twice,
teardown is bounded to 500 ms, and a rejecting or hung teardown is
swallowed so the reload always runs — a rejection can no longer win the
race and strand the renderer on a stale heap.

All fire/withhold logic is a pure, exhaustively tested predicate; the hook
only wires live signals, arms after the session-age threshold, and
serializes overlapping checks. Signals are read imperatively at check time
(once a minute) so the hook holds no reactive subscription and never
re-renders. The controller mounts inside the ready main-window shell only,
guarded so Huddle companion windows and every onboarding/blocking/reset/
keyring/relaunch screen are ineligible by construction.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 force-pushed the hayt/idle-auto-reload branch from e671093 to 863f8a0 Compare August 11, 2026 18:50
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