All files / src/services sessionManager.ts

79.41% Statements 54/68
69.56% Branches 16/23
60% Functions 9/15
80.59% Lines 54/67

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143  40x 40x               40x 40x 40x 40x 40x     11x           3x               2x                     5x           5x 2x 2x     2x 2x 2x     3x     40x                       40x 20x     40x             40x 5x 5x 5x 5x     40x           40x 3x 3x 3x 3x 3x     40x 6x 5x 5x 5x     5x   5x 5x 3x 3x             3x 3x 3x 3x 3x   2x 2x   2x   5x         6x    
import type { AuthSessionResponse, StoredAuthSession } from '@/models/auth';
import { API_BASE_URL } from '@/config/apiBaseUrl';
import {
  clearStoredSession,
  readStoredSession,
  writeStoredSession,
} from '@/services/sessionStorage';
 
type SessionListener = (session: StoredAuthSession | null) => void;
 
let currentSession: StoredAuthSession | null = null;
let hydratePromise: Promise<StoredAuthSession | null> | null = null;
let refreshPromise: Promise<StoredAuthSession> | null = null;
let sessionVersion = 0;
const listeners = new Set<SessionListener>();
 
function notifyListeners() {
  for (const listener of listeners) {
    listener(currentSession);
  }
}
 
function mapAuthSession(response: AuthSessionResponse): StoredAuthSession {
  return {
    access_token: response.access_token,
    refresh_token: response.refresh_token,
    user: response.user,
  };
}
 
function isTerminalRefreshError(error: unknown): boolean {
  return (
    typeof error === 'object' &&
    error !== null &&
    'status' in error &&
    (error as { status?: unknown }).status === 401
  );
}
 
async function refreshSessionRequest(
  refreshToken: string,
): Promise<AuthSessionResponse> {
  const response = await fetch(`${API_BASE_URL}/auth/refresh`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ refresh_token: refreshToken }),
  });
 
  if (!response.ok) {
    const body = await response.json().catch(() => null);
    const error = new Error(
      body?.error?.message ?? 'Could not refresh the session.',
    ) as Error & { status?: number; code?: string };
    error.status = response.status;
    error.code = body?.error?.code;
    throw error;
  }
 
  return response.json();
}
 
export async function hydrateSession(): Promise<StoredAuthSession | null> {
  if (!hydratePromise) {
    hydratePromise = (async () => {
      currentSession = await readStoredSession();
      notifyListeners();
      return currentSession;
    })();
  }
 
  return hydratePromise;
}
 
export function getCurrentSession(): StoredAuthSession | null {
  return currentSession;
}
 
export function subscribeToSession(listener: SessionListener): () => void {
  listeners.add(listener);
  return () => {
    listeners.delete(listener);
  };
}
 
export async function setSession(session: StoredAuthSession): Promise<void> {
  await writeStoredSession(session);
  sessionVersion += 1;
  currentSession = session;
  notifyListeners();
}
 
export async function setSessionFromAuthResponse(
  response: AuthSessionResponse,
): Promise<void> {
  await setSession(mapAuthSession(response));
}
 
export async function clearSession(): Promise<void> {
  refreshPromise = null;
  await clearStoredSession();
  sessionVersion += 1;
  currentSession = null;
  notifyListeners();
}
 
export async function refreshSession(): Promise<StoredAuthSession> {
  if (!refreshPromise) {
    refreshPromise = (async () => {
      const session = currentSession ?? (await hydrateSession());
      Iif (!session?.refresh_token) {
        throw new Error('No refresh token available');
      }
      const refreshVersion = sessionVersion;
 
      try {
        const refreshed = await refreshSessionRequest(session.refresh_token);
        const nextSession = mapAuthSession(refreshed);
        Iif (
          currentSession === null ||
          sessionVersion !== refreshVersion ||
          currentSession.refresh_token !== session.refresh_token
        ) {
          throw new Error('Session changed during refresh');
        }
        await writeStoredSession(nextSession);
        sessionVersion += 1;
        currentSession = nextSession;
        notifyListeners();
        return nextSession;
      } catch (error) {
        Eif (isTerminalRefreshError(error)) {
          await clearSession();
        }
        throw error;
      } finally {
        refreshPromise = null;
      }
    })();
  }
 
  return refreshPromise;
}