@oxyhq/services
Version:
294 lines (289 loc) • 10.8 kB
JavaScript
;
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