Phase 3 (security): - P3.1: Feature gate server-side enforcement (403 on disabled features) - P3.2: Unified auth error interceptor (401→logout) - P3.3: Store-level state isolation via StoreSnapshot contract (added message-list-tabs + task stores to snapshot/restore cycle) - P3.4: Push event bus extraction — email-store no longer imports calendar/contact/filter/file stores directly - P1.3: Auth localStorage AES-GCM encryption via custom Zustand adapter Phase 4 (polish): - P4.1: Offline write queue — pending operations in localStorage, auto-retry on reconnect, offline-queue-indicator banner - P4.2: Identity spoofing — fromOverrideEmail domain validation - P4.3: WebSocket push for Electron via main-process IPC bridge (ws package with Authorization headers)
167 lines
5.4 KiB
TypeScript
167 lines
5.4 KiB
TypeScript
/**
|
|
* Manages per-account state snapshots for fast switching.
|
|
* When user switches from Account A → B, we snapshot A's store state
|
|
* into memory, clear stores, then restore B's cached state.
|
|
*/
|
|
|
|
import { useEmailStore } from '@/stores/email-store';
|
|
import { useContactStore } from '@/stores/contact-store';
|
|
import { useCalendarStore } from '@/stores/calendar-store';
|
|
import { useFilterStore } from '@/stores/filter-store';
|
|
import { DEFAULT_SEARCH_FILTERS } from '@/lib/jmap/search-utils';
|
|
import { useIdentityStore } from '@/stores/identity-store';
|
|
import { useVacationStore } from '@/stores/vacation-store';
|
|
import { useMessageListTabsStore } from '@/stores/message-list-tabs-store';
|
|
import { useTaskStore } from '@/stores/task-store';
|
|
|
|
export interface StoreSnapshot<S> {
|
|
snapshot: () => Partial<S>;
|
|
clear: () => Partial<S>;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
type StoreData = Record<string, any>;
|
|
|
|
interface AccountSnapshot {
|
|
email: StoreData;
|
|
contact: StoreData;
|
|
calendar: StoreData;
|
|
filter: StoreData;
|
|
identity: StoreData;
|
|
vacation: StoreData;
|
|
messageListTabs: StoreData;
|
|
tasks: StoreData;
|
|
}
|
|
|
|
const cache = new Map<string, AccountSnapshot>();
|
|
|
|
/** Capture current store states for the given account */
|
|
export function snapshotAccount(accountId: string): void {
|
|
const emailState = useEmailStore.getState();
|
|
const contactState = useContactStore.getState();
|
|
const calendarState = useCalendarStore.getState();
|
|
const filterState = useFilterStore.getState();
|
|
const identityState = useIdentityStore.getState();
|
|
const vacationState = useVacationStore.getState();
|
|
const messageListTabsState = useMessageListTabsStore.getState();
|
|
const taskState = useTaskStore.getState();
|
|
|
|
cache.set(accountId, {
|
|
email: {
|
|
emails: [...emailState.emails],
|
|
mailboxes: [...emailState.mailboxes],
|
|
selectedEmail: emailState.selectedEmail,
|
|
selectedMailbox: emailState.selectedMailbox,
|
|
searchQuery: emailState.searchQuery,
|
|
quota: emailState.quota ? { ...emailState.quota } : emailState.quota,
|
|
},
|
|
contact: {
|
|
contacts: [...contactState.contacts],
|
|
addressBooks: [...contactState.addressBooks],
|
|
supportsSync: contactState.supportsSync,
|
|
},
|
|
calendar: {
|
|
calendars: [...calendarState.calendars],
|
|
events: [...calendarState.events],
|
|
selectedCalendarIds: [...calendarState.selectedCalendarIds],
|
|
viewMode: calendarState.viewMode,
|
|
supportsCalendar: calendarState.supportsCalendar,
|
|
},
|
|
filter: {
|
|
rules: [...filterState.rules],
|
|
isSupported: filterState.isSupported,
|
|
},
|
|
identity: {
|
|
identities: [...identityState.identities],
|
|
preferredPrimaryId: identityState.preferredPrimaryId,
|
|
},
|
|
vacation: {
|
|
isEnabled: vacationState.isEnabled,
|
|
isSupported: vacationState.isSupported,
|
|
},
|
|
messageListTabs: {
|
|
registrations: { ...messageListTabsState.registrations },
|
|
tabs: [...messageListTabsState.tabs],
|
|
activeTabId: messageListTabsState.activeTabId,
|
|
},
|
|
tasks: {
|
|
tasks: [...taskState.tasks],
|
|
selectedTaskId: taskState.selectedTaskId,
|
|
filter: taskState.filter,
|
|
showCompleted: taskState.showCompleted,
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Restore cached store states for the given account. Returns false if no cache
|
|
* exists.
|
|
*
|
|
* The snapshot only captures a subset of each store's fields (the loaded data),
|
|
* so we reset every store to its baseline first. Without this, fields outside
|
|
* the captured subset (e.g. email selection, loading flags, tag counts) would
|
|
* carry over from whatever account was active, leaking state across accounts.
|
|
* `setState` merges, so the captured fields are then layered back on top.
|
|
*/
|
|
export function restoreAccount(accountId: string): boolean {
|
|
const snapshot = cache.get(accountId);
|
|
if (!snapshot) return false;
|
|
|
|
clearAllStores();
|
|
|
|
useEmailStore.setState(snapshot.email);
|
|
useContactStore.setState(snapshot.contact);
|
|
useCalendarStore.setState(snapshot.calendar);
|
|
useFilterStore.setState(snapshot.filter);
|
|
useIdentityStore.setState(snapshot.identity);
|
|
useVacationStore.setState(snapshot.vacation);
|
|
useMessageListTabsStore.setState(snapshot.messageListTabs);
|
|
useTaskStore.setState(snapshot.tasks);
|
|
|
|
return true;
|
|
}
|
|
|
|
/** Clear all stores (used before restoring a different account) */
|
|
export function clearAllStores(): void {
|
|
useEmailStore.setState({
|
|
emails: [],
|
|
mailboxes: [],
|
|
selectedEmail: null,
|
|
selectedMailbox: '',
|
|
isLoading: false,
|
|
error: null,
|
|
searchQuery: '',
|
|
quota: null,
|
|
isPushConnected: false,
|
|
lastPushUpdate: null,
|
|
newEmailNotification: null,
|
|
selectedEmailIds: new Set<string>(),
|
|
hasMoreEmails: false,
|
|
totalEmails: 0,
|
|
expandedThreadIds: new Set<string>(),
|
|
threadEmailsCache: new Map(),
|
|
isLoadingThread: null,
|
|
selectedKeyword: null,
|
|
tagCounts: {},
|
|
searchFilters: { ...DEFAULT_SEARCH_FILTERS },
|
|
isAdvancedSearchOpen: false,
|
|
});
|
|
useIdentityStore.getState().clearIdentities();
|
|
useContactStore.getState().clearContacts();
|
|
useVacationStore.getState().clearState();
|
|
useCalendarStore.getState().clearState();
|
|
useFilterStore.getState().clearState();
|
|
useMessageListTabsStore.getState().clearState();
|
|
useTaskStore.getState().clearTasks();
|
|
}
|
|
|
|
/** Evict cached state for one account */
|
|
export function evictAccount(accountId: string): void {
|
|
cache.delete(accountId);
|
|
}
|
|
|
|
/** Evict all cached states */
|
|
export function evictAll(): void {
|
|
cache.clear();
|
|
}
|