@oxyhq/services
Version:
Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀
286 lines (232 loc) • 10.4 kB
text/typescript
import { create } from 'zustand';
import { shallow } from 'zustand/shallow';
import type { OxyServices } from '../../core';
export interface QuickAccount {
sessionId: string;
userId?: string; // User ID for deduplication
username: string;
displayName: string;
avatar?: string;
avatarUrl?: string; // Cached avatar URL to prevent recalculation
}
interface AccountState {
// Account data
accounts: Record<string, QuickAccount>;
accountOrder: string[]; // Maintain order for display
accountsArray: QuickAccount[]; // Cached array to prevent infinite loops
// Loading states
loading: boolean;
loadingSessionIds: Set<string>;
// Error state
error: string | null;
// Actions
setAccounts: (accounts: QuickAccount[]) => void;
addAccount: (account: QuickAccount) => void;
updateAccount: (sessionId: string, updates: Partial<QuickAccount>) => void;
removeAccount: (sessionId: string) => void;
moveAccountToTop: (sessionId: string) => void;
// Loading actions
setLoading: (loading: boolean) => void;
setLoadingSession: (sessionId: string, loading: boolean) => void;
// Error actions
setError: (error: string | null) => void;
// Load accounts from API
loadAccounts: (sessionIds: string[], oxyServices: OxyServices, existingAccounts?: QuickAccount[], preserveOrder?: boolean) => Promise<void>;
// Reset store
reset: () => void;
}
const initialState = {
accounts: {} as Record<string, QuickAccount>,
accountOrder: [] as string[],
accountsArray: [] as QuickAccount[],
loading: false,
loadingSessionIds: new Set<string>(),
error: null,
};
// Helper: Build accounts array from accounts map and order
const buildAccountsArray = (accounts: Record<string, QuickAccount>, order: string[]): QuickAccount[] => {
const result: QuickAccount[] = [];
for (const id of order) {
const account = accounts[id];
if (account) result.push(account);
}
return result;
};
// Helper: Create QuickAccount from user data
const createQuickAccount = (sessionId: string, userData: any, existingAccount?: QuickAccount, oxyServices?: OxyServices): QuickAccount => {
const displayName = userData.name?.full || userData.name?.first || userData.username || 'Account';
const userId = userData.id || userData._id?.toString();
// Preserve existing avatarUrl if avatar hasn't changed (prevents image reload)
let avatarUrl: string | undefined;
if (existingAccount && existingAccount.avatar === userData.avatar && existingAccount.avatarUrl) {
avatarUrl = existingAccount.avatarUrl; // Reuse existing URL
} else if (userData.avatar && oxyServices) {
avatarUrl = oxyServices.getFileDownloadUrl(userData.avatar, 'thumb');
}
return {
sessionId,
userId,
username: userData.username || '',
displayName,
avatar: userData.avatar,
avatarUrl,
};
};
export const useAccountStore = create<AccountState>((set, get) => ({
...initialState,
setAccounts: (accounts) => set((state) => {
const accountMap: Record<string, QuickAccount> = {};
const order: string[] = [];
const seenSessionIds = new Set<string>();
for (const account of accounts) {
if (seenSessionIds.has(account.sessionId)) continue;
seenSessionIds.add(account.sessionId);
accountMap[account.sessionId] = account;
order.push(account.sessionId);
}
const accountsArray = buildAccountsArray(accountMap, order);
const sameOrder = order.length === state.accountOrder.length &&
order.every((id, i) => id === state.accountOrder[i]);
const sameAccounts = sameOrder &&
order.every(id => {
const existing = state.accounts[id];
const newAccount = accountMap[id];
return existing &&
existing.sessionId === newAccount.sessionId &&
existing.userId === newAccount.userId &&
existing.avatar === newAccount.avatar &&
existing.avatarUrl === newAccount.avatarUrl;
});
if (sameAccounts) return {} as any;
return { accounts: accountMap, accountOrder: order, accountsArray };
}),
addAccount: (account) => set((state) => {
// Check if account with same sessionId exists
if (state.accounts[account.sessionId]) {
// Update existing
const existing = state.accounts[account.sessionId];
if (existing.avatar === account.avatar && existing.avatarUrl === account.avatarUrl) {
return {} as any; // No change
}
const newAccounts = { ...state.accounts, [account.sessionId]: account };
return {
accounts: newAccounts,
accountsArray: buildAccountsArray(newAccounts, state.accountOrder),
};
}
const newAccounts = { ...state.accounts, [account.sessionId]: account };
const newOrder = [account.sessionId, ...state.accountOrder];
return {
accounts: newAccounts,
accountOrder: newOrder,
accountsArray: buildAccountsArray(newAccounts, newOrder),
};
}),
updateAccount: (sessionId, updates) => set((state) => {
const existing = state.accounts[sessionId];
if (!existing) return {} as any;
const updated = { ...existing, ...updates };
if (existing.avatar === updated.avatar && existing.avatarUrl === updated.avatarUrl) {
return {} as any; // No change
}
const newAccounts = { ...state.accounts, [sessionId]: updated };
return {
accounts: newAccounts,
accountsArray: buildAccountsArray(newAccounts, state.accountOrder),
};
}),
removeAccount: (sessionId) => set((state) => {
if (!state.accounts[sessionId]) return {} as any;
const { [sessionId]: _removed, ...rest } = state.accounts;
const newOrder = state.accountOrder.filter(id => id !== sessionId);
return {
accounts: rest,
accountOrder: newOrder,
accountsArray: buildAccountsArray(rest, newOrder),
};
}),
moveAccountToTop: (sessionId) => set((state) => {
if (!state.accounts[sessionId]) return {} as any;
const filtered = state.accountOrder.filter(id => id !== sessionId);
const newOrder = [sessionId, ...filtered];
return {
accountOrder: newOrder,
accountsArray: buildAccountsArray(state.accounts, newOrder),
};
}),
setLoading: (loading) => set({ loading }),
setLoadingSession: (sessionId, loading) => set((state) => {
const newSet = new Set(state.loadingSessionIds);
if (loading) {
newSet.add(sessionId);
} else {
newSet.delete(sessionId);
}
return { loadingSessionIds: newSet };
}),
setError: (error) => set({ error }),
loadAccounts: async (sessionIds, oxyServices, existingAccounts = [], preserveOrder = true) => {
const state = get();
const uniqueSessionIds = Array.from(new Set(sessionIds));
if (uniqueSessionIds.length === 0) {
get().setAccounts([]);
return;
}
const existingMap = new Map(existingAccounts.map(a => [a.sessionId, a]));
for (const account of Object.values(state.accounts)) {
existingMap.set(account.sessionId, account);
}
const missingSessionIds = uniqueSessionIds.filter(id => !existingMap.has(id));
if (missingSessionIds.length === 0) {
const ordered = uniqueSessionIds
.map(id => existingMap.get(id))
.filter((acc): acc is QuickAccount => acc !== undefined);
get().setAccounts(ordered);
return;
}
if (state.loading) {
return;
}
set({ loading: true, error: null });
try {
const batchResults = await oxyServices.getUsersBySessions(missingSessionIds);
const accountMap = new Map<string, QuickAccount>();
for (const { sessionId, user: userData } of batchResults) {
if (userData && !accountMap.has(sessionId)) {
const existing = existingMap.get(sessionId);
accountMap.set(sessionId, createQuickAccount(sessionId, userData, existing, oxyServices));
}
}
for (const [sessionId, account] of accountMap) {
existingMap.set(sessionId, account);
}
const orderToUse = preserveOrder ? uniqueSessionIds : [...uniqueSessionIds, ...state.accountOrder];
const seen = new Set<string>();
const ordered: QuickAccount[] = [];
for (const sessionId of orderToUse) {
if (seen.has(sessionId)) continue;
seen.add(sessionId);
const account = existingMap.get(sessionId);
if (account) ordered.push(account);
}
get().setAccounts(ordered);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to load accounts';
if (__DEV__) {
console.error('AccountStore: Failed to load accounts:', error);
}
set({ error: errorMessage });
} finally {
set({ loading: false });
}
},
reset: () => set(initialState),
}));
// Selectors for performance - return cached array to prevent infinite loops
export const useAccounts = (): QuickAccount[] => {
return useAccountStore(state => state.accountsArray);
};
export const useAccountLoading = () => useAccountStore(s => s.loading);
export const useAccountError = () => useAccountStore(s => s.error);
export const useAccountLoadingSession = (sessionId: string) =>
useAccountStore(s => s.loadingSessionIds.has(sessionId));