From a57ed65d3b0e27f9fcf3d2d2e4536c0b8d3dcba8 Mon Sep 17 00:00:00 2001 From: npub1cl47vfhsqpqy9pwndphpm36vcp7vvz5h2js4qpqm5yewzj7nutkq7xyw8c Date: Thu, 23 Jul 2026 11:06:42 -0700 Subject: [PATCH] Preserve community data on onboarding write failure Co-authored-by: npub1cl47vfhsqpqy9pwndphpm36vcp7vvz5h2js4qpqm5yewzj7nutkq7xyw8c Signed-off-by: npub1cl47vfhsqpqy9pwndphpm36vcp7vvz5h2js4qpqm5yewzj7nutkq7xyw8c --- .../communities/communityStorage.test.mjs | 24 +++++++++++++++++ .../features/communities/communityStorage.ts | 26 ++++++++++++++----- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/desktop/src/features/communities/communityStorage.test.mjs b/desktop/src/features/communities/communityStorage.test.mjs index 170ae11882..6533cc4357 100644 --- a/desktop/src/features/communities/communityStorage.test.mjs +++ b/desktop/src/features/communities/communityStorage.test.mjs @@ -3,6 +3,7 @@ import test from "node:test"; import { clearCommunityStorage, + initFirstCommunity, migrateLegacyCommunityStorage, shouldAutoConnectDefaultRelay, } from "./communityStorage.ts"; @@ -10,6 +11,7 @@ import { function createMemoryStorage(initial = {}) { const values = new Map(Object.entries(initial)); return { + values, getItem: (key) => values.get(key) ?? null, setItem: (key, value) => values.set(key, String(value)), removeItem: (key) => values.delete(key), @@ -59,6 +61,28 @@ test("signed-build relay defaults auto-connect during first-run onboarding", () assert.equal(shouldAutoConnectDefaultRelay("not a valid relay"), false); }); +test("failed first-community write preserves existing community data", () => { + const storage = createMemoryStorage({ + "buzz-communities": '[{"id":"existing"}]', + "buzz-workspaces": '[{"id":"legacy"}]', + "buzz-active-workspace-id": "legacy", + }); + storage.setItem = (key, value) => { + if (key === "buzz-communities") { + throw new Error("QuotaExceededError"); + } + storage.values.set(key, String(value)); + }; + globalThis.localStorage = storage; + globalThis.window = { localStorage: storage }; + + assert.equal(initFirstCommunity("wss://relay.example.com", "pubkey"), null); + assert.equal(storage.getItem("buzz-communities"), '[{"id":"existing"}]'); + assert.equal(storage.getItem("buzz-active-community-id"), null); + assert.equal(storage.getItem("buzz-workspaces"), '[{"id":"legacy"}]'); + assert.equal(storage.getItem("buzz-active-workspace-id"), "legacy"); +}); + test("clearCommunityStorage removes new and legacy state", () => { const storage = createMemoryStorage({ "buzz-communities": "new", diff --git a/desktop/src/features/communities/communityStorage.ts b/desktop/src/features/communities/communityStorage.ts index 9f4d79eb47..b39d312734 100644 --- a/desktop/src/features/communities/communityStorage.ts +++ b/desktop/src/features/communities/communityStorage.ts @@ -160,14 +160,26 @@ export function initFirstCommunity( pubkey, addedAt: new Date().toISOString(), }; - const didSaveCommunities = saveCommunities([community]); - const didSaveActiveCommunity = - didSaveCommunities && saveActiveCommunityId(community.id); + const previousActiveCommunityId = localStorage.getItem(ACTIVE_COMMUNITY_KEY); + const didSaveActiveCommunity = saveActiveCommunityId(community.id); + if (!didSaveActiveCommunity) { + return null; + } - // The quota-recovery helper reports failure instead of throwing. Do not let - // callers reload unless both writes landed; otherwise first launch can loop. - if (!didSaveCommunities || !didSaveActiveCommunity) { - clearCommunityStorage(); + if (!saveCommunities([community])) { + // A failed setItem leaves the existing communities value untouched. Roll + // back only the active-ID write so inconsistent pre-existing data is never + // destroyed while recovering from a quota failure. + try { + if (previousActiveCommunityId === null) { + localStorage.removeItem(ACTIVE_COMMUNITY_KEY); + } else { + localStorage.setItem(ACTIVE_COMMUNITY_KEY, previousActiveCommunityId); + } + } catch { + // Best effort: persistence is already unavailable, and callers will stay + // on setup instead of reloading. + } return null; }