Files
buzz/desktop/src/main.tsx
T
bb9aae1065 feat(desktop): time-based sweep for stale localStorage caches (#5453)
Part of #5418 (Phase 1, lane B).

## What

Adds a periodic, whitelist-driven TTL sweep for disposable localStorage
caches so a desktop session left open for days converges to the same
storage state as one restarted nightly.

- New `desktop/src/shared/lib/localStorageSweep.ts`: declarative
`LOCAL_STORAGE_SWEEP_RULES` table — six repaintable pure-cache prefixes
(matching `PURE_CACHE_KEY_PREFIXES` in `localStorageQuota.ts`), all
14-day TTL, keyed on each payload's `updatedAt` (user-label buckets use
their newest nested per-profile timestamp).
- Entries with no trustworthy timestamp are retained, never guessed
stale. `buzz-self-profile.v1:` is deliberately excluded — it is the
load-bearing offline identity fallback (guard comment in the table).
- Scheduler: first sweep deferred off the boot critical path via
`requestIdleCallback` (1.5s timeout) with a 250ms timer fallback, then
hourly and on return-to-visible, debounced to 5 minutes. Throw-safe
throughout (failures `console.warn`, never crash — per `safeStorage.ts`
conventions / #5078).
- Wired in `desktop/src/main.tsx` beside
`recoverLocalStorageQuotaOnStartup()`.

## Validation

- Focused node test 7/7 at HEAD; pre-push gate green (desktop-check,
desktop-typecheck, full desktop-test 4542/4542).
- Manual Playwright (not covered by push hooks):
`relay-connectivity.spec.ts -g "04"` (offline cached identity) passes
1/1 at HEAD — this spec caught and now guards the v1 regression.
- Independent adversarial review: FULL REVIEW (REQUEST CHANGES) then
VERIFIED — PASS at exactly this commit, including whitelist containment
against the 58-site inventory, scheduler tracing, and smoke E2E.

Authored by Summer (agent), reviewed by Beth (agent), integrated by Rick
(agent). Discussion: Buzz channel time-based-localstorage-eviction,
thread 0d85a73ca43e54748128f89c3512a4726131bf5473253395d46bf8f3a7b58bd4.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Summer <1fdd3cc104e2911eb3b2da6f97d1b25f4a7f3550ded4492b24ff1d95acd66766@buzz.block.builderlab.xyz>
2026-08-10 09:47:04 -07:00

133 lines
4.6 KiB
TypeScript

import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "@/app/App";
import { RootErrorBoundary } from "@/app/RootErrorBoundary";
import { NostrBindConsentDialog } from "@/features/profile/ui/NostrBindConsentDialog";
import "@fontsource-variable/inter/wght.css";
import "@fontsource/jetbrains-mono/400.css";
import "@fontsource/jetbrains-mono/700.css";
import "@/shared/styles/globals.css";
import { UpdaterProvider } from "@/features/settings/hooks/UpdaterProvider";
import { migrateLegacyCommunityStorageBeforeRender } from "@/features/communities/legacyCommunityStorage";
import { CommunitiesProvider } from "@/features/communities/useCommunities";
import { huddleWindowChannelId } from "@/features/huddle/lib/huddleWindow";
import { CommunityOnboardingProvider } from "@/features/onboarding/communityOnboarding";
import { ThemeProvider } from "@/shared/theme/ThemeProvider";
import { EmojiBurstProvider } from "@/shared/ui/EmojiBurstProvider";
import { PoofBurstProvider } from "@/shared/ui/PoofBurstProvider";
import { Toaster } from "@/shared/ui/sonner";
import { TooltipProvider } from "@/shared/ui/tooltip";
import { recoverLocalStorageQuotaOnStartup } from "@/shared/lib/localStorageQuota";
import { startLocalStorageSweep } from "@/shared/lib/localStorageSweep";
type E2eWindow = Window & {
__BUZZ_E2E__?: unknown;
};
const E2E_DEFAULT_PUBKEY = "deadbeef".repeat(8);
const E2E_COMMUNITY_ID = "e2e-default-community";
const ONBOARDING_COMPLETION_STORAGE_KEY_PREFIX = "buzz-onboarding-complete.v1:";
const DEV_STATE_RESET_PARAM = "resetDevState";
function resetDevWebviewStateFromUrl() {
if (!import.meta.env.DEV) {
return;
}
const url = new URL(window.location.href);
if (url.searchParams.get(DEV_STATE_RESET_PARAM) !== "1") {
return;
}
// WebKit groups every Buzz binary under one disk directory, but storage is
// isolated by origin. Clearing here resets only this dev server's origin;
// deleting the shared WebKit directory would also destroy installed-app state.
window.localStorage.clear();
window.sessionStorage.clear();
url.searchParams.delete(DEV_STATE_RESET_PARAM);
window.history.replaceState(window.history.state, "", url);
}
function configureDevE2eBridgeFromUrl() {
if (!import.meta.env.DEV) {
return;
}
const url = new URL(window.location.href);
if (url.searchParams.get("e2e") !== "mock") {
return;
}
const e2eWindow = window as E2eWindow;
e2eWindow.__BUZZ_E2E__ ??= { mode: "mock" };
const community = {
addedAt: new Date().toISOString(),
id: E2E_COMMUNITY_ID,
name: "E2E Test",
relayUrl: "ws://localhost:3000",
};
window.localStorage.setItem("buzz-communities", JSON.stringify([community]));
window.localStorage.setItem("buzz-active-community-id", E2E_COMMUNITY_ID);
window.localStorage.setItem(
`${ONBOARDING_COMPLETION_STORAGE_KEY_PREFIX}${E2E_DEFAULT_PUBKEY}`,
"true",
);
}
function renderApp() {
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
{/* block/buzz#5078 — catch any uncaught render error so a WebKit
SecurityError from localStorage can't blank the whole window. */}
<RootErrorBoundary>
<CommunitiesProvider>
<CommunityOnboardingProvider
enabled={huddleWindowChannelId() === null}
>
<ThemeProvider defaultTheme="buzz">
<TooltipProvider delayDuration={300}>
<EmojiBurstProvider>
<PoofBurstProvider>
<UpdaterProvider>
<App />
<NostrBindConsentDialog />
</UpdaterProvider>
<Toaster />
</PoofBurstProvider>
</EmojiBurstProvider>
</TooltipProvider>
</ThemeProvider>
</CommunityOnboardingProvider>
</CommunitiesProvider>
</RootErrorBoundary>
</React.StrictMode>,
);
}
async function installE2eBridgeIfConfigured() {
// The mock bridge is compiled only into dev and explicit E2E builds. A
// pre-bootstrap global alone must never activate mock IPC in production.
if (
!(import.meta.env.DEV || import.meta.env.MODE === "e2e") ||
!(window as E2eWindow).__BUZZ_E2E__
) {
return;
}
const { maybeInstallE2eTauriMocks } = await import("@/testing/e2eBridge");
maybeInstallE2eTauriMocks();
}
async function bootstrap() {
resetDevWebviewStateFromUrl();
configureDevE2eBridgeFromUrl();
recoverLocalStorageQuotaOnStartup();
startLocalStorageSweep();
await installE2eBridgeIfConfigured();
await migrateLegacyCommunityStorageBeforeRender();
renderApp();
}
void bootstrap();