Files
c6c6e7eca7 perf(desktop): coalesce thread-activity localStorage writes (#5693)
Each incoming thread reply drove a full `JSON.stringify` + `setItem` of
the ~600 KB thread-activity buffer. A burst of replies serialized the
whole blob once per event on the main thread, which is one of the
renderer stalls under load in the desktop-longevity arc.

This collapses the burst into a single debounced write, applying the
coalescing pattern Wes introduced for read-state persistence in #5591
(`readStateManager`) to the thread-activity path.

## What changed

- **`threadActivityStorage.ts`** — coalescing primitives:
- `scheduleThreadActivityWrite` — first-writer-wins (a pending timer is
*not* reset), 1s trailing edge. The timer reads the live buffer *at fire
time* and re-checks the loaded scope, so N replies within the window
persist exactly once with the burst's final state, and a write that
outlives a scope switch can neither land under the new key nor persist
the wrong buffer.
- `flushThreadActivityWrite` — synchronous persist + timer cancel; a
no-op when nothing is pending.
- `removeLegacyThreadActivityKey` — idempotent one-time cleanup of the
orphaned pre-relay-scoping `buzz-thread-activity.v1:<pubkey>` key.
- **`useThreadActivityPersistence.ts`** (new companion hook) — owns the
loaded scope, the write timer, the `pagehide` /
`visibilitychange`→hidden / unmount flush, and hydration + legacy
cleanup on identity/relay change. Mirrors the existing
`useObservedUnreadPersistence` sibling.
- **`useUnreadChannels.ts`** — rewired to instantiate the hook and call
`activityPersistence.schedule(...)` at both writer sites instead of
writing per event. The buffer (`threadActivityRef`) stays parent-owned;
the hook decides when it is durably persisted. Net **990** lines (was
1021), back under the 1000-line ceiling.

## Durability

`pagehide`, `visibilitychange`→hidden, unmount, and scope-reseed all
flush synchronously, so the last burst of replies survives a `Cmd+R` or
an idle reload that tears the webview down inside the coalescing window.

## Tests

- `threadActivityWriteScheduler.test.mjs` — fake-timer unit coverage:
burst→one `setItem`, live-buffer-at-fire-time, scope-mismatch rejection,
stale-scope timer abort, flush persists+cancels, flush no-op, legacy-key
removal.
- `useThreadActivityPersistence.test.mjs` — mounts the real hook via
`createRoot`+`act`: `pagehide` / visibility / unmount flush of the live
buffer, scope switch flushing A under A's key without leaking into B,
B-bucket rehydration, legacy-key cleanup, and the empty-scope write
fence.

## Related

Based on [#5591](https://github.com/block/buzz/pull/5591) (Wes) —
`perf(desktop): coalesce read state localStorage persistence`, the
proven first-writer-wins coalescing pattern this extends to thread
activity.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-12 12:06:43 -07:00
..
2026-08-03 21:51:17 -04:00

Buzz

Desktop chat shell with:

  • Tauri + React + TypeScript + Vite
  • Tailwind CSS
  • shadcn/ui-ready shared components
  • Biome (lint/format/check)
  • Feature-driven frontend structure

Scripts

  • pnpm dev - run the web frontend
  • pnpm tauri dev - run the desktop app
  • pnpm build - typecheck and build frontend
  • pnpm typecheck - TypeScript checks
  • pnpm lint - Biome lint
  • pnpm format - Biome format (write)
  • pnpm check - Biome check

Structure

  • src/shared - reusable app-wide code (ui, lib, styles)
  • src/features - feature modules (vertical slices)
  • src/app - top-level app composition