UNPKG

@oxyhq/services

Version:

OxyHQ Expo/React Native SDK — UI components, screens, and native features

294 lines (289 loc) 10.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useSessionManagement = void 0; var _react = require("react"); var _core = require("@oxyhq/core"); var _sessionHelpers = require("../utils/sessionHelpers.js"); var _storageHelpers = require("../utils/storageHelpers.js"); var _errorHandlers = require("../utils/errorHandlers.js"); var _queryClient = require("./queryClient.js"); const DEFAULT_SAVE_ERROR_MESSAGE = 'Failed to save session data'; const CLEAR_STORAGE_ERROR = 'Failed to clear storage'; /** * Manage session state, persistence, and high-level multi-session operations. * * @param options - Session management configuration */ const useSessionManagement = ({ oxyServices, storage, storageKeyPrefix, loginSuccess, logoutStore, applyLanguagePreference, onAuthStateChange, onError, setAuthError, logger, setTokenReady, queryClient }) => { const [sessions, setSessions] = (0, _react.useState)([]); const [activeSessionId, setActiveSessionId] = (0, _react.useState)(null); // Refs to avoid recreating callbacks when sessions/activeSessionId change const sessionsRef = (0, _react.useRef)(sessions); sessionsRef.current = sessions; const activeSessionIdRef = (0, _react.useRef)(activeSessionId); activeSessionIdRef.current = activeSessionId; const refreshInFlightRef = (0, _react.useRef)(null); const removedSessionsRef = (0, _react.useRef)(new Set()); const lastRefreshRef = (0, _react.useRef)(0); const storageKeys = (0, _react.useMemo)(() => (0, _storageHelpers.getStorageKeys)(storageKeyPrefix), [storageKeyPrefix]); const saveSessionIds = (0, _react.useCallback)(async sessionIds => { if (!storage) return; try { const uniqueIds = Array.from(new Set(sessionIds)); await storage.setItem(storageKeys.sessionIds, JSON.stringify(uniqueIds)); } catch (error) { if (logger) { logger(DEFAULT_SAVE_ERROR_MESSAGE, error); } else if (__DEV__) { console.warn('Failed to save session IDs:', error); } } }, [logger, storage, storageKeys.sessionIds]); const updateSessions = (0, _react.useCallback)((incoming, options = {}) => { setSessions(prevSessions => { const processed = options.merge ? (0, _core.mergeSessions)(prevSessions, incoming, activeSessionIdRef.current, false) : (0, _core.normalizeAndSortSessions)(incoming, activeSessionIdRef.current, false); if (storage) { void saveSessionIds(processed.map(session => session.sessionId)); } if ((0, _core.sessionsArraysEqual)(prevSessions, processed)) { return prevSessions; } return processed; }); }, [saveSessionIds, storage]); const saveActiveSessionId = (0, _react.useCallback)(async sessionId => { if (!storage) return; try { await storage.setItem(storageKeys.activeSessionId, sessionId); } catch (error) { (0, _errorHandlers.handleAuthError)(error, { defaultMessage: DEFAULT_SAVE_ERROR_MESSAGE, code: 'SESSION_PERSISTENCE_ERROR', onError, setAuthError, logger }); } }, [logger, onError, setAuthError, storage, storageKeys.activeSessionId]); const removeActiveSessionId = (0, _react.useCallback)(async () => { if (!storage) return; try { await storage.removeItem(storageKeys.activeSessionId); } catch (error) { (0, _errorHandlers.handleAuthError)(error, { defaultMessage: DEFAULT_SAVE_ERROR_MESSAGE, code: 'SESSION_PERSISTENCE_ERROR', onError, setAuthError, logger }); } }, [logger, onError, setAuthError, storage, storageKeys.activeSessionId]); const clearSessionStorage = (0, _react.useCallback)(async () => { if (!storage) return; try { await storage.removeItem(storageKeys.activeSessionId); await storage.removeItem(storageKeys.sessionIds); // Note: Identity sync state ('oxy_identity_synced') is managed by accounts app } catch (error) { (0, _errorHandlers.handleAuthError)(error, { defaultMessage: CLEAR_STORAGE_ERROR, code: 'STORAGE_ERROR', onError, setAuthError, logger }); } }, [logger, onError, setAuthError, storage, storageKeys.activeSessionId, storageKeys.sessionIds]); const clearSessionState = (0, _react.useCallback)(async () => { setSessions([]); setActiveSessionId(null); logoutStore(); // Clear TanStack Query cache (in-memory) if (queryClient) { queryClient.clear(); } // Clear persisted query cache if (storage) { try { await (0, _queryClient.clearQueryCache)(storage); } catch (error) { if (logger) { logger('Failed to clear persisted query cache', error); } } } await clearSessionStorage(); onAuthStateChange?.(null); }, [clearSessionStorage, logoutStore, onAuthStateChange, queryClient, storage, logger]); const activateSession = (0, _react.useCallback)(async (sessionId, user) => { await oxyServices.getTokenBySession(sessionId); setTokenReady?.(true); setActiveSessionId(sessionId); loginSuccess(user); await saveActiveSessionId(sessionId); await applyLanguagePreference(user); onAuthStateChange?.(user); }, [applyLanguagePreference, loginSuccess, onAuthStateChange, oxyServices, saveActiveSessionId, setTokenReady]); const removalTimerIdsRef = (0, _react.useRef)(new Set()); const trackRemovedSession = (0, _react.useCallback)(sessionId => { removedSessionsRef.current.add(sessionId); const timerId = setTimeout(() => { removedSessionsRef.current.delete(sessionId); removalTimerIdsRef.current.delete(timerId); }, 5000); removalTimerIdsRef.current.add(timerId); }, []); (0, _react.useEffect)(() => { return () => { removalTimerIdsRef.current.forEach(clearTimeout); }; }, []); const findReplacementSession = (0, _react.useCallback)(async sessionIds => { if (!sessionIds.length) { return null; } const validationResults = await (0, _sessionHelpers.validateSessionBatch)(oxyServices, sessionIds, { maxConcurrency: 3 }); const validSession = validationResults.find(result => result.valid); if (!validSession) { return null; } const validation = await oxyServices.validateSession(validSession.sessionId, { useHeaderValidation: true }); if (!validation?.valid || !validation.user) { return null; } const user = validation.user; await activateSession(validSession.sessionId, user); return user; }, [activateSession, oxyServices]); const switchSession = (0, _react.useCallback)(async sessionId => { try { const validation = await oxyServices.validateSession(sessionId, { useHeaderValidation: true }); if (!validation?.valid) { throw new Error('Session is invalid or expired'); } if (!validation.user) { throw new Error('User data not available from session validation'); } const user = validation.user; await activateSession(sessionId, user); try { const deviceSessions = await (0, _sessionHelpers.fetchSessionsWithFallback)(oxyServices, sessionId, { fallbackUserId: user.id, logger }); updateSessions(deviceSessions, { merge: true }); } catch (error) { if (__DEV__) { console.warn('Failed to synchronize sessions after switch:', error); } } return user; } catch (error) { const invalidSession = (0, _errorHandlers.isInvalidSessionError)(error); if (invalidSession) { updateSessions(sessionsRef.current.filter(session => session.sessionId !== sessionId), { merge: false }); if (sessionId === activeSessionIdRef.current) { const otherSessionIds = sessionsRef.current.filter(session => session.sessionId !== sessionId && !removedSessionsRef.current.has(session.sessionId)).map(session => session.sessionId); const replacementUser = await findReplacementSession(otherSessionIds); if (replacementUser) { return replacementUser; } } } (0, _errorHandlers.handleAuthError)(error, { defaultMessage: 'Failed to switch session', code: invalidSession ? 'INVALID_SESSION' : 'SESSION_SWITCH_ERROR', onError, setAuthError, logger }); throw error instanceof Error ? error : new Error('Failed to switch session'); } }, [activateSession, findReplacementSession, logger, onError, oxyServices, setAuthError, updateSessions]); const refreshSessions = (0, _react.useCallback)(async activeUserId => { if (!activeSessionIdRef.current) return; if (refreshInFlightRef.current) { await refreshInFlightRef.current; return; } const now = Date.now(); if (now - lastRefreshRef.current < 500) { return; } lastRefreshRef.current = now; const refreshPromise = (async () => { try { const deviceSessions = await (0, _sessionHelpers.fetchSessionsWithFallback)(oxyServices, activeSessionIdRef.current, { fallbackUserId: activeUserId, logger }); updateSessions(deviceSessions, { merge: true }); } catch (error) { if ((0, _errorHandlers.isInvalidSessionError)(error)) { const otherSessions = sessionsRef.current.filter(session => session.sessionId !== activeSessionIdRef.current && !removedSessionsRef.current.has(session.sessionId)).map(session => session.sessionId); const replacementUser = await findReplacementSession(otherSessions); if (!replacementUser) { await clearSessionState(); } return; } (0, _errorHandlers.handleAuthError)(error, { defaultMessage: 'Failed to refresh sessions', code: 'SESSION_REFRESH_ERROR', onError, setAuthError, logger }); } finally { refreshInFlightRef.current = null; lastRefreshRef.current = Date.now(); } })(); refreshInFlightRef.current = refreshPromise; await refreshPromise; }, [clearSessionState, findReplacementSession, logger, onError, oxyServices, setAuthError, updateSessions]); const isRefreshInFlight = Boolean(refreshInFlightRef.current); return { sessions, activeSessionId, setActiveSessionId, updateSessions, switchSession, refreshSessions, clearSessionState, saveActiveSessionId, trackRemovedSession, storageKeys, isRefreshInFlight }; }; exports.useSessionManagement = useSessionManagement; //# sourceMappingURL=useSessionManagement.js.map