Amba-Agent-App/lib/stores/userProfileStore.ts
2026-01-16 00:22:35 +03:00

259 lines
8.3 KiB
TypeScript

/**
* User Profile Store - Platform-aware user profile management
* Uses Firebase abstraction layer for cross-platform support
*/
import { create } from 'zustand';
import { doc } from '../firebase';
import type { UserProfile } from '../services/authServices';
export interface ProfileCacheEntry {
profile: UserProfile | null;
loading: boolean;
error: string | null;
lastFetched?: number;
}
interface UserProfileStoreState {
profiles: Record<string, ProfileCacheEntry>;
subscriptions: Record<string, () => void>;
ensureSubscription: (uid: string) => void;
refreshProfile: (uid: string) => Promise<UserProfile | null>;
invalidateProfile: (uid: string) => Promise<UserProfile | null>;
removeProfile: (uid: string) => void;
clearAll: () => void;
}
const buildEntry = (
existing: ProfileCacheEntry | undefined,
overrides: Partial<ProfileCacheEntry>
): ProfileCacheEntry => ({
profile: existing?.profile ?? null,
loading: existing?.loading ?? false,
error: existing?.error ?? null,
lastFetched: existing?.lastFetched,
...overrides,
});
const snapshotExists = (docSnap: any): boolean => {
const existsValue = docSnap?.exists;
if (typeof existsValue === 'function') {
try {
return !!existsValue.call(docSnap);
} catch {
return false;
}
}
return !!existsValue;
};
export const useUserProfileStore = create<UserProfileStoreState>((set, get) => ({
profiles: {},
subscriptions: {},
ensureSubscription: (uid: string) => {
if (!uid) {
return;
}
// If already subscribed, don't create another subscription
if (get().subscriptions[uid]) {
return;
}
const existing = get().profiles[uid];
if (existing?.loading) {
return;
}
// If we have cached data, set up a real-time listener
if (existing?.lastFetched) {
const userDocRef = doc('users', uid);
const unsubscribe = userDocRef.onSnapshot(
(docSnap: any) => {
const profile = snapshotExists(docSnap) ? (docSnap.data() as UserProfile) : null;
set((state) => ({
profiles: {
...state.profiles,
[uid]: buildEntry(state.profiles[uid], {
profile,
loading: false,
error: profile ? null : 'Profile not found',
lastFetched: Date.now(),
}),
},
}));
},
(error: any) => {
set((state) => ({
profiles: {
...state.profiles,
[uid]: buildEntry(state.profiles[uid], {
loading: false,
error: error.message || 'Failed to fetch profile',
}),
},
}));
}
);
set((state) => ({
subscriptions: {
...state.subscriptions,
[uid]: unsubscribe,
},
}));
return;
}
// Initial fetch
void get().refreshProfile(uid).catch((error) => {
set((state) => ({
profiles: {
...state.profiles,
[uid]: buildEntry(state.profiles[uid], {
loading: false,
error: error instanceof Error ? error.message : 'Failed to fetch profile',
}),
},
}));
});
},
refreshProfile: async (uid: string) => {
if (!uid) {
return null;
}
set((state) => ({
profiles: {
...state.profiles,
[uid]: buildEntry(state.profiles[uid], {
loading: true,
error: null,
}),
},
}));
try {
const userDocRef = doc('users', uid);
const docSnap = await userDocRef.get();
const profile = snapshotExists(docSnap) ? (docSnap.data() as UserProfile) : null;
set((state) => ({
profiles: {
...state.profiles,
[uid]: buildEntry(state.profiles[uid], {
profile,
loading: false,
error: profile ? null : 'Profile not found',
lastFetched: Date.now(),
}),
},
}));
// Set up real-time listener after initial fetch if not already subscribed
if (!get().subscriptions[uid] && profile) {
const unsubscribe = userDocRef.onSnapshot(
(docSnap: any) => {
const updatedProfile = snapshotExists(docSnap) ? (docSnap.data() as UserProfile) : null;
set((state) => ({
profiles: {
...state.profiles,
[uid]: buildEntry(state.profiles[uid], {
profile: updatedProfile,
loading: false,
error: updatedProfile ? null : 'Profile not found',
lastFetched: Date.now(),
}),
},
}));
},
(error: any) => {
set((state) => ({
profiles: {
...state.profiles,
[uid]: buildEntry(state.profiles[uid], {
loading: false,
error: error.message || 'Failed to fetch profile',
}),
},
}));
}
);
set((state) => ({
subscriptions: {
...state.subscriptions,
[uid]: unsubscribe,
},
}));
}
return profile;
} catch (error) {
set((state) => ({
profiles: {
...state.profiles,
[uid]: buildEntry(state.profiles[uid], {
loading: false,
error: error instanceof Error ? error.message : 'Failed to fetch profile',
}),
},
}));
throw error;
}
},
invalidateProfile: async (uid: string) => {
if (!uid) {
return null;
}
set((state) => ({
profiles: {
...state.profiles,
[uid]: buildEntry(state.profiles[uid], {
loading: true,
error: null,
lastFetched: undefined,
}),
},
}));
return get().refreshProfile(uid);
},
removeProfile: (uid: string) => {
// Unsubscribe from listener if exists
const unsubscribe = get().subscriptions[uid];
if (unsubscribe) {
unsubscribe();
}
set((state) => {
const { [uid]: _removedProfile, ...restProfiles } = state.profiles;
const { [uid]: _removedSub, ...restSubs } = state.subscriptions;
return {
profiles: restProfiles,
subscriptions: restSubs,
};
});
},
clearAll: () => {
// Unsubscribe from all listeners
const subscriptions = get().subscriptions;
Object.values(subscriptions).forEach((unsubscribe) => {
unsubscribe();
});
set({ profiles: {}, subscriptions: {} });
},
}));
export const getUserProfileFromCache = (uid: string): UserProfile | null => {
if (!uid) {
return null;
}
return useUserProfileStore.getState().profiles[uid]?.profile ?? null;
};