mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
fix(desktop): scope draft store per workspace relay (#2179)
This commit is contained in:
@@ -6,7 +6,10 @@ import { getIdentity } from "@/shared/api/tauriIdentity";
|
||||
import { getOverrides } from "@/shared/features";
|
||||
import { resetMediaCaches } from "@/shared/lib/mediaUrl";
|
||||
import { clearSearchHitEventCache } from "@/app/navigation/searchHitEventCache";
|
||||
import { initDraftStore } from "@/features/messages/lib/useDrafts";
|
||||
import {
|
||||
clearAllDrafts,
|
||||
initDraftStore,
|
||||
} from "@/features/messages/lib/useDrafts";
|
||||
import { resetRenderScopedReactionHydration } from "@/features/messages/lib/renderScopedReactions";
|
||||
import {
|
||||
resetActiveAgentTurnsStore,
|
||||
@@ -31,6 +34,7 @@ import type { Community } from "./types";
|
||||
*/
|
||||
function resetCommunityState(): void {
|
||||
relayClient.disconnect();
|
||||
clearAllDrafts();
|
||||
resetAgentObserverStore();
|
||||
resetActiveAgentTurnsStore();
|
||||
resetAgentWorkingSignal();
|
||||
@@ -193,10 +197,16 @@ export function useCommunityInit(
|
||||
// and bypass the localhost proxy.
|
||||
resetMediaCaches();
|
||||
|
||||
// Initialise the draft store for this identity so localStorage drafts
|
||||
// are scoped to the correct pubkey before the app renders.
|
||||
if (activeCommunity.pubkey) {
|
||||
initDraftStore(activeCommunity.pubkey);
|
||||
try {
|
||||
const identity = await getIdentity();
|
||||
if (cancelled) return;
|
||||
initDraftStore(identity.pubkey, activeCommunity.relayUrl);
|
||||
} catch (err) {
|
||||
if (cancelled) return;
|
||||
console.error(
|
||||
"[useCommunityInit] getIdentity failed, draft store uninitialized:",
|
||||
err,
|
||||
);
|
||||
}
|
||||
// Restore any turn state saved for this community (a prior A→B round-
|
||||
// trip). This runs after applyCommunity succeeds and before the app
|
||||
|
||||
@@ -630,7 +630,7 @@ function makeFullImeta(overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
/** Read the raw persisted store blob for the current pubkey. */
|
||||
/** Read the raw persisted store blob for the current pubkey (legacy v1 key). */
|
||||
function readRawStore(pubkey) {
|
||||
const raw = localStorage.getItem(`buzz-drafts.v1:${pubkey}`);
|
||||
return raw ? JSON.parse(raw) : {};
|
||||
@@ -985,3 +985,409 @@ test("invalid_mention_ref_rejects_corrupt_draft", () => {
|
||||
initDraftStore("corrupt-mention-owner");
|
||||
assert.equal(loadDraftEntry("chan-corrupt"), undefined);
|
||||
});
|
||||
|
||||
// ── Relay-scoped draft store (v2 key) ─────────────────────────────────────────
|
||||
|
||||
test("cross_relay_isolation_via_initDraftStore_without_clearAllDrafts", () => {
|
||||
const storage = installFreshLocalStorage();
|
||||
clearAllDrafts();
|
||||
const pk = "pubkey-shared";
|
||||
|
||||
// Setup: write drafts in two relay scopes via direct initDraftStore calls
|
||||
// with NO intervening clearAllDrafts — this pins the production invariant
|
||||
// that initDraftStore resets _memCache when relayScope changes.
|
||||
initDraftStore(pk, "wss://relay-a.example.com");
|
||||
saveDraftEntry("chan-1", makeDraft({ content: "Draft on relay A" }));
|
||||
assert.equal(loadDraftEntry("chan-1")?.content, "Draft on relay A");
|
||||
|
||||
// Switch to relay B directly — no clearAllDrafts.
|
||||
initDraftStore(pk, "wss://relay-b.example.com");
|
||||
assert.equal(
|
||||
loadDraftEntry("chan-1"),
|
||||
undefined,
|
||||
"relay B must not see relay A drafts",
|
||||
);
|
||||
|
||||
saveDraftEntry("chan-1", makeDraft({ content: "Draft on relay B" }));
|
||||
assert.equal(loadDraftEntry("chan-1")?.content, "Draft on relay B");
|
||||
|
||||
// Switch back to relay A directly — no clearAllDrafts.
|
||||
initDraftStore(pk, "wss://relay-a.example.com");
|
||||
assert.equal(
|
||||
loadDraftEntry("chan-1")?.content,
|
||||
"Draft on relay A",
|
||||
"relay A draft must survive relay B session",
|
||||
);
|
||||
|
||||
// Verify localStorage has two distinct keys.
|
||||
const v2a = storage.getItem(
|
||||
"buzz-drafts.v2:wss://relay-a.example.com:pubkey-shared",
|
||||
);
|
||||
const v2b = storage.getItem(
|
||||
"buzz-drafts.v2:wss://relay-b.example.com:pubkey-shared",
|
||||
);
|
||||
assert.ok(v2a, "v2 key for relay A must exist");
|
||||
assert.ok(v2b, "v2 key for relay B must exist");
|
||||
assert.notEqual(v2a, v2b, "buckets must differ");
|
||||
});
|
||||
|
||||
test("legacy_migration_v1_entries_readable_after_scoped_init_v1_key_removed", () => {
|
||||
const storage = installFreshLocalStorage();
|
||||
clearAllDrafts();
|
||||
const pk = "pubkey-migrating";
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Seed a v1 bucket (pre-upgrade state).
|
||||
storage.setItem(
|
||||
`buzz-drafts.v1:${pk}`,
|
||||
JSON.stringify({
|
||||
"chan-old": {
|
||||
content: "Legacy draft",
|
||||
selectionStart: 12,
|
||||
selectionEnd: 12,
|
||||
channelId: "chan-old",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
pendingImeta: [],
|
||||
spoileredAttachmentUrls: [],
|
||||
status: "active",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Init with a relay scope — should trigger migration.
|
||||
initDraftStore(pk, "wss://relay.example.com");
|
||||
|
||||
// Legacy draft must be readable through the scoped store.
|
||||
const loaded = loadDraftEntry("chan-old");
|
||||
assert.ok(loaded, "migrated draft must be readable");
|
||||
assert.equal(loaded.content, "Legacy draft");
|
||||
|
||||
// v1 key must be deleted after migration.
|
||||
assert.equal(
|
||||
storage.getItem(`buzz-drafts.v1:${pk}`),
|
||||
null,
|
||||
"v1 key must be removed after migration",
|
||||
);
|
||||
|
||||
// v2 key must exist.
|
||||
const v2 = storage.getItem(`buzz-drafts.v2:wss://relay.example.com:${pk}`);
|
||||
assert.ok(v2, "v2 key must hold the migrated data");
|
||||
});
|
||||
|
||||
test("legacy_migration_second_relay_init_does_not_reimport_deleted_v1", () => {
|
||||
const storage = installFreshLocalStorage();
|
||||
clearAllDrafts();
|
||||
const pk = "pubkey-double-migrate";
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Seed v1.
|
||||
storage.setItem(
|
||||
`buzz-drafts.v1:${pk}`,
|
||||
JSON.stringify({
|
||||
"chan-first": {
|
||||
content: "First workspace draft",
|
||||
selectionStart: 0,
|
||||
selectionEnd: 0,
|
||||
channelId: "chan-first",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
pendingImeta: [],
|
||||
spoileredAttachmentUrls: [],
|
||||
status: "active",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// First workspace loads — migrates v1 into relay-a's v2 bucket.
|
||||
initDraftStore(pk, "wss://relay-a.example.com");
|
||||
assert.ok(loadDraftEntry("chan-first"), "relay A got the legacy draft");
|
||||
|
||||
// v1 key is gone.
|
||||
assert.equal(storage.getItem(`buzz-drafts.v1:${pk}`), null);
|
||||
|
||||
// Second workspace initializes — must NOT see the legacy draft.
|
||||
clearAllDrafts();
|
||||
initDraftStore(pk, "wss://relay-b.example.com");
|
||||
assert.equal(
|
||||
loadDraftEntry("chan-first"),
|
||||
undefined,
|
||||
"relay B must not inherit legacy drafts already migrated to relay A",
|
||||
);
|
||||
});
|
||||
|
||||
test("v2_already_exists_legacy_bucket_ignored", () => {
|
||||
const storage = installFreshLocalStorage();
|
||||
clearAllDrafts();
|
||||
const pk = "pubkey-v2-exists";
|
||||
const now = new Date().toISOString();
|
||||
const relay = "wss://relay.example.com";
|
||||
|
||||
// Seed both v1 and v2 — simulates a user who already ran post-upgrade
|
||||
// but still has a leftover v1 key.
|
||||
storage.setItem(
|
||||
`buzz-drafts.v1:${pk}`,
|
||||
JSON.stringify({
|
||||
"chan-legacy": {
|
||||
content: "Should be ignored",
|
||||
selectionStart: 0,
|
||||
selectionEnd: 0,
|
||||
channelId: "chan-legacy",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
pendingImeta: [],
|
||||
spoileredAttachmentUrls: [],
|
||||
status: "active",
|
||||
},
|
||||
}),
|
||||
);
|
||||
storage.setItem(
|
||||
`buzz-drafts.v2:${relay}:${pk}`,
|
||||
JSON.stringify({
|
||||
"chan-v2": {
|
||||
content: "V2 draft",
|
||||
selectionStart: 8,
|
||||
selectionEnd: 8,
|
||||
channelId: "chan-v2",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
pendingImeta: [],
|
||||
spoileredAttachmentUrls: [],
|
||||
status: "active",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
initDraftStore(pk, relay);
|
||||
|
||||
// v2 content wins.
|
||||
assert.equal(loadDraftEntry("chan-v2")?.content, "V2 draft");
|
||||
// Legacy content NOT merged.
|
||||
assert.equal(
|
||||
loadDraftEntry("chan-legacy"),
|
||||
undefined,
|
||||
"v1 entries must not leak when v2 key exists",
|
||||
);
|
||||
// v1 key is NOT deleted (no migration path ran).
|
||||
assert.ok(
|
||||
storage.getItem(`buzz-drafts.v1:${pk}`),
|
||||
"v1 key preserved when v2 already existed (no migration)",
|
||||
);
|
||||
});
|
||||
|
||||
// ── F1: clearAllDrafts fail-closed teardown ──────────────────────────────────
|
||||
|
||||
test("clearAllDrafts_without_reinit_blocks_reads_and_writes_to_previous_bucket", () => {
|
||||
const storage = installFreshLocalStorage();
|
||||
clearAllDrafts();
|
||||
const pk = "pubkey-teardown";
|
||||
|
||||
// Init relay A, write a draft.
|
||||
initDraftStore(pk, "wss://relay-a.example.com");
|
||||
saveDraftEntry("chan-1", makeDraft({ content: "A's draft" }));
|
||||
assert.equal(loadDraftEntry("chan-1")?.content, "A's draft");
|
||||
|
||||
// Snapshot A's bucket before teardown.
|
||||
const bucketBefore = storage.getItem(
|
||||
"buzz-drafts.v2:wss://relay-a.example.com:pubkey-teardown",
|
||||
);
|
||||
|
||||
// Simulate community switch teardown — clearAllDrafts with NO re-init.
|
||||
clearAllDrafts();
|
||||
|
||||
// Reads must return undefined (no active scope).
|
||||
assert.equal(
|
||||
loadDraftEntry("chan-1"),
|
||||
undefined,
|
||||
"reads must return undefined after clearAllDrafts without re-init",
|
||||
);
|
||||
|
||||
// A write attempt must not modify A's stored bucket.
|
||||
saveDraftEntry("chan-1", makeDraft({ content: "Rogue write" }));
|
||||
assert.equal(
|
||||
storage.getItem("buzz-drafts.v2:wss://relay-a.example.com:pubkey-teardown"),
|
||||
bucketBefore,
|
||||
"A's stored bucket must be byte-unchanged after unscoped write",
|
||||
);
|
||||
|
||||
// Re-init with relay B — must see a clean bucket, not A's drafts.
|
||||
initDraftStore(pk, "wss://relay-b.example.com");
|
||||
assert.equal(
|
||||
loadDraftEntry("chan-1"),
|
||||
undefined,
|
||||
"relay B must not see relay A drafts after teardown + re-init",
|
||||
);
|
||||
});
|
||||
|
||||
// ── F2: failed v2 flush preserves the only v1 copy ──────────────────────────
|
||||
|
||||
test("failed_v2_flush_during_migration_preserves_v1_key", () => {
|
||||
const storage = installFreshLocalStorage();
|
||||
clearAllDrafts();
|
||||
const pk = "pubkey-flush-fail";
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Seed v1 bucket.
|
||||
storage.setItem(
|
||||
`buzz-drafts.v1:${pk}`,
|
||||
JSON.stringify({
|
||||
"chan-legacy": {
|
||||
content: "Precious legacy",
|
||||
selectionStart: 0,
|
||||
selectionEnd: 0,
|
||||
channelId: "chan-legacy",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
pendingImeta: [],
|
||||
spoileredAttachmentUrls: [],
|
||||
status: "active",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Force setItem to throw persistently (quota full, no recovery possible).
|
||||
const origSetItem = storage.setItem;
|
||||
storage.setItem = () => {
|
||||
throw new DOMException("QuotaExceededError");
|
||||
};
|
||||
|
||||
// Init with relay scope — triggers migration attempt.
|
||||
initDraftStore(pk, "wss://relay.example.com");
|
||||
|
||||
// Restore setItem so assertions can read storage.
|
||||
storage.setItem = origSetItem;
|
||||
|
||||
// v1 key must still be present (flush failed, so legacy NOT deleted).
|
||||
assert.ok(
|
||||
storage.getItem(`buzz-drafts.v1:${pk}`),
|
||||
"v1 key must be preserved when v2 flush fails",
|
||||
);
|
||||
|
||||
// v2 key must NOT exist (flush failed).
|
||||
assert.equal(
|
||||
storage.getItem(`buzz-drafts.v2:wss://relay.example.com:${pk}`),
|
||||
null,
|
||||
"v2 key must not exist when flush failed",
|
||||
);
|
||||
});
|
||||
|
||||
test("successful_v2_flush_during_migration_deletes_v1_key", () => {
|
||||
const storage = installFreshLocalStorage();
|
||||
clearAllDrafts();
|
||||
const pk = "pubkey-flush-ok";
|
||||
const now = new Date().toISOString();
|
||||
|
||||
storage.setItem(
|
||||
`buzz-drafts.v1:${pk}`,
|
||||
JSON.stringify({
|
||||
"chan-old": {
|
||||
content: "Migrating",
|
||||
selectionStart: 0,
|
||||
selectionEnd: 0,
|
||||
channelId: "chan-old",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
pendingImeta: [],
|
||||
spoileredAttachmentUrls: [],
|
||||
status: "active",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
initDraftStore(pk, "wss://relay.example.com");
|
||||
|
||||
assert.equal(
|
||||
storage.getItem(`buzz-drafts.v1:${pk}`),
|
||||
null,
|
||||
"v1 key deleted after successful migration",
|
||||
);
|
||||
assert.ok(
|
||||
storage.getItem(`buzz-drafts.v2:wss://relay.example.com:${pk}`),
|
||||
"v2 key created after successful migration",
|
||||
);
|
||||
assert.equal(loadDraftEntry("chan-old")?.content, "Migrating");
|
||||
});
|
||||
|
||||
// ── F4: relay URL canonicalization ──────────────────────────────────────────
|
||||
|
||||
test("relay_canonicalization_host_case_and_trailing_slash_coalesce", () => {
|
||||
installFreshLocalStorage();
|
||||
clearAllDrafts();
|
||||
const pk = "pubkey-canon";
|
||||
|
||||
// Write with mixed-case host.
|
||||
initDraftStore(pk, "WSS://Relay.Example.COM/");
|
||||
saveDraftEntry("chan-1", makeDraft({ content: "canonical draft" }));
|
||||
|
||||
// Re-init with lowercase + no trailing slash — must see same draft.
|
||||
initDraftStore(pk, "wss://relay.example.com");
|
||||
assert.equal(
|
||||
loadDraftEntry("chan-1")?.content,
|
||||
"canonical draft",
|
||||
"host-case + trailing-slash variants must coalesce",
|
||||
);
|
||||
});
|
||||
|
||||
test("relay_canonicalization_path_case_stays_isolated", () => {
|
||||
installFreshLocalStorage();
|
||||
clearAllDrafts();
|
||||
const pk = "pubkey-path-case";
|
||||
|
||||
initDraftStore(pk, "wss://gateway.example.com/Team");
|
||||
saveDraftEntry("chan-1", makeDraft({ content: "Team draft" }));
|
||||
|
||||
// Different path case — must NOT see the draft.
|
||||
initDraftStore(pk, "wss://gateway.example.com/team");
|
||||
assert.equal(
|
||||
loadDraftEntry("chan-1"),
|
||||
undefined,
|
||||
"path-case variants must remain isolated",
|
||||
);
|
||||
|
||||
// Original path case still intact.
|
||||
initDraftStore(pk, "wss://gateway.example.com/Team");
|
||||
assert.equal(
|
||||
loadDraftEntry("chan-1")?.content,
|
||||
"Team draft",
|
||||
"original path-case draft must survive",
|
||||
);
|
||||
});
|
||||
|
||||
test("no_relay_legacy_caller_form_still_reads_writes_v1_key", () => {
|
||||
const storage = installFreshLocalStorage();
|
||||
clearAllDrafts();
|
||||
const pk = "pubkey-no-relay";
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Seed a v1 bucket.
|
||||
storage.setItem(
|
||||
`buzz-drafts.v1:${pk}`,
|
||||
JSON.stringify({
|
||||
"chan-legacy": {
|
||||
content: "Legacy no-relay",
|
||||
selectionStart: 0,
|
||||
selectionEnd: 0,
|
||||
channelId: "chan-legacy",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
pendingImeta: [],
|
||||
spoileredAttachmentUrls: [],
|
||||
status: "active",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Init with no relay (legacy caller form).
|
||||
initDraftStore(pk);
|
||||
|
||||
// Must read from v1 key.
|
||||
assert.equal(loadDraftEntry("chan-legacy")?.content, "Legacy no-relay");
|
||||
|
||||
// Write a new draft.
|
||||
saveDraftEntry("chan-new", makeDraft({ content: "New no-relay" }));
|
||||
|
||||
// Verify it lands in the v1 key.
|
||||
const raw = JSON.parse(storage.getItem(`buzz-drafts.v1:${pk}`));
|
||||
assert.ok(raw["chan-new"], "new draft must be in v1 key");
|
||||
assert.equal(raw["chan-new"].content, "New no-relay");
|
||||
});
|
||||
|
||||
@@ -79,28 +79,57 @@ export type DraftState = {
|
||||
/** Serialised shape stored in localStorage (same as DraftState for round-trips). */
|
||||
type StoredDrafts = Record<string, DraftState>;
|
||||
|
||||
const DRAFT_STORE_KEY_PREFIX = "buzz-drafts.v1";
|
||||
const DRAFT_STORE_KEY_PREFIX = "buzz-drafts.v2";
|
||||
const LEGACY_DRAFT_STORE_KEY_PREFIX = "buzz-drafts.v1";
|
||||
const MAX_DRAFTS = 100;
|
||||
|
||||
/** Module-level pubkey set by `initDraftStore`. Empty string = no identity. */
|
||||
/**
|
||||
* Canonicalize a relay URL for use as a storage key scope.
|
||||
* Unlike the shared `normalizeRelayUrl` (which lowercases the entire URL),
|
||||
* this preserves path/query case so that distinct path-bearing relays
|
||||
* (e.g. `wss://host/Team` vs `wss://host/team`) produce separate buckets.
|
||||
*/
|
||||
function canonicalizeRelayScope(relayUrl: string): string {
|
||||
const trimmed = relayUrl.trim().replace(/\/+$/, "");
|
||||
if (!trimmed) return "";
|
||||
try {
|
||||
const u = new URL(trimmed);
|
||||
const path = u.pathname.replace(/\/+$/, "");
|
||||
return `${u.protocol}//${u.host}${path}${u.search}`;
|
||||
} catch {
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
/** Module-level workspace identity set by `initDraftStore`. Empty = no workspace. */
|
||||
let currentPubkey = "";
|
||||
let currentRelayScope = "";
|
||||
|
||||
function storageKey(): string {
|
||||
return `${DRAFT_STORE_KEY_PREFIX}:${currentPubkey}`;
|
||||
// The no-relay form is retained for direct legacy callers/tests. App startup
|
||||
// always supplies a normalized relay and therefore uses the v2 scoped key.
|
||||
return currentRelayScope
|
||||
? `${DRAFT_STORE_KEY_PREFIX}:${currentRelayScope}:${currentPubkey}`
|
||||
: legacyStorageKey();
|
||||
}
|
||||
|
||||
function legacyStorageKey(): string {
|
||||
return `${LEGACY_DRAFT_STORE_KEY_PREFIX}:${currentPubkey}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise (or re-initialise) the draft store for a given identity.
|
||||
* Called from `useCommunityInit` alongside the other singleton resets.
|
||||
* Resets the in-memory cache whenever the pubkey changes so a direct
|
||||
* identity switch (without a prior `clearAllDrafts`) never serves the
|
||||
* wrong identity's drafts.
|
||||
* Resets the in-memory cache whenever the pubkey or relay scope changes
|
||||
* so a workspace switch never serves the wrong workspace's drafts.
|
||||
*/
|
||||
export function initDraftStore(pubkey: string): void {
|
||||
if (currentPubkey !== pubkey) {
|
||||
export function initDraftStore(pubkey: string, relayUrl = ""): void {
|
||||
const relayScope = canonicalizeRelayScope(relayUrl);
|
||||
if (currentPubkey !== pubkey || currentRelayScope !== relayScope) {
|
||||
_memCache = null;
|
||||
}
|
||||
currentPubkey = pubkey;
|
||||
currentRelayScope = relayScope;
|
||||
// Eagerly load to surface corruption errors in console at startup rather
|
||||
// than on first draft interaction.
|
||||
readStore();
|
||||
@@ -112,6 +141,7 @@ export function initDraftStore(pubkey: string): void {
|
||||
*/
|
||||
export function clearAllDrafts(): void {
|
||||
currentPubkey = "";
|
||||
currentRelayScope = "";
|
||||
_memCache = null;
|
||||
}
|
||||
|
||||
@@ -131,13 +161,19 @@ function readStore(): Map<string, DraftState> {
|
||||
}
|
||||
|
||||
const raw = localStorage.getItem(storageKey());
|
||||
if (!raw) {
|
||||
// One-time forward migration: read legacy v1 entries only when no v2 store
|
||||
// exists yet AND a relay scope is set. Once migrated, the v1 key is deleted
|
||||
// so no other workspace can import the same legacy bucket.
|
||||
const legacyRaw =
|
||||
raw || !currentRelayScope ? null : localStorage.getItem(legacyStorageKey());
|
||||
const source = raw ?? legacyRaw;
|
||||
if (!source) {
|
||||
_memCache = map;
|
||||
return map;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(raw);
|
||||
const parsed: unknown = JSON.parse(source);
|
||||
if (
|
||||
parsed !== null &&
|
||||
typeof parsed === "object" &&
|
||||
@@ -160,6 +196,11 @@ function readStore(): Map<string, DraftState> {
|
||||
}
|
||||
|
||||
_memCache = map;
|
||||
if (legacyRaw !== null) {
|
||||
if (flushStore(map)) {
|
||||
localStorage.removeItem(legacyStorageKey());
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -210,13 +251,13 @@ function isValidDraftState(v: unknown): v is DraftState {
|
||||
return true;
|
||||
}
|
||||
|
||||
function flushStore(map: Map<string, DraftState>): void {
|
||||
if (!currentPubkey) return;
|
||||
function flushStore(map: Map<string, DraftState>): boolean {
|
||||
if (!currentPubkey) return false;
|
||||
const obj: StoredDrafts = {};
|
||||
for (const [k, v] of map) {
|
||||
obj[k] = v;
|
||||
}
|
||||
setLocalStorageItemWithRecovery(storageKey(), JSON.stringify(obj));
|
||||
return setLocalStorageItemWithRecovery(storageKey(), JSON.stringify(obj));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user