/** * REFERENCE TOOL: Svelte + SvelteKit Best Practices & Patterns * * Every Svelte file follows these patterns. No exceptions. */ // ============================================================================ // PATTERN 1: Svelte Store Pattern // ============================================================================ /** * STORE: authStore * PURPOSE: Centralized auth state (user, tokens, login status) * UPDATES: Called only through explicit update functions * SUBSCRIBERS: Any component needing auth state * * Files reference this store: components that check auth, routes that require auth */ // File: src/stores/auth.ts import { writable, derived } from 'svelte/store'; /** * USER STORE * Holds current user info from Microsoft */ export const user = writable(null); /** * ACCESS TOKEN STORE * Holds current access token (short-lived, 1 hour) * RULE: Cleared when token expires * RULE: Never persisted to localStorage */ export const accessToken = writable(null); /** * TOKEN EXPIRY STORE * When does current token expire (timestamp in ms) */ export const tokenExpiry = writable(null); /** * DERIVED: Is authenticated? * Returns true if user exists and token is not expired */ export const isAuthenticated = derived( [user, tokenExpiry], ([$user, $expiry]) => { return !!$user && $expiry && Date.now() < $expiry; } ); /** * UPDATE FUNCTION: Set user after login * * CALLED BY: OAuth callback handler * UPDATES: user, accessToken, tokenExpiry * SIDE EFFECTS: Stores token in sessionStorage, sets up refresh timer */ export async function setAuthUser( userInfo: User, token: string, expiresIn: number ): Promise { // Update stores user.set(userInfo); accessToken.set(token); tokenExpiry.set(Date.now() + expiresIn * 1000); // Store in sessionStorage (safe, lost on browser close) sessionStorage.setItem('ms_access_token', token); sessionStorage.setItem('ms_token_expiry', (Date.now() + expiresIn * 1000).toString()); // Set up automatic refresh timer (5 minutes before expiry) const refreshIn = (expiresIn * 1000) - (5 * 60 * 1000); setTimeout(() => { refreshAccessToken(); }, refreshIn); console.log('Auth user set:', userInfo.displayName); } /** * UPDATE FUNCTION: Refresh token via backend * * CALLED BY: Automatic timer (5 min before expiry), before API calls * SIDE EFFECTS: Updates accessToken, tokenExpiry, sessionStorage */ export async function refreshAccessToken(): Promise { try { const response = await fetch('/api/auth/refresh', { method: 'POST', headers: { 'Authorization': `Bearer ${get(user)?.id}` }, }); if (!response.ok) { // Token refresh failed clearAuth(); // Force re-login throw new Error('Token refresh failed'); } const { accessToken: newToken, expiresIn } = await response.json(); // Update stores accessToken.set(newToken); tokenExpiry.set(Date.now() + expiresIn * 1000); sessionStorage.setItem('ms_access_token', newToken); sessionStorage.setItem('ms_token_expiry', (Date.now() + expiresIn * 1000).toString()); // Reset refresh timer setTimeout(() => { refreshAccessToken(); }, (expiresIn * 1000) - (5 * 60 * 1000)); console.log('Access token refreshed'); } catch (error) { console.error('Token refresh error:', error); clearAuth(); } } /** * UPDATE FUNCTION: Logout * * CALLED BY: Logout button, auth failures * SIDE EFFECTS: Clears all auth stores, sessionStorage, redirects */ export async function clearAuth(): Promise { user.set(null); accessToken.set(null); tokenExpiry.set(null); sessionStorage.removeItem('ms_access_token'); sessionStorage.removeItem('ms_token_expiry'); // Call backend logout (optional, clears refresh token from PocketBase) try { await fetch('/api/auth/logout', { method: 'POST' }); } catch (e) { console.error('Logout API error:', e); } console.log('Auth cleared'); } // ============================================================================ // PATTERN 2: Svelte Component with Auth Requirement // ============================================================================ /** * COMPONENT: JobList.svelte * PURPOSE: Display list of jobs from Microsoft Graph * REQUIRES: User to be authenticated * * Reactive: $isAuthenticated, $jobs, searchQuery * Events: None (pure presentation) * API calls: Loads jobs via GraphService */ // File: src/components/JobList.svelte {#if !$isAuthenticated}
Please sign in to view jobs
{:else if $jobsLoading}
Loading jobs...
{:else if filteredJobs.length === 0}
No jobs found matching your search
{:else}
{#each filteredJobs as job (job.id)} {/each}
{/if} // ============================================================================ // PATTERN 3: API Service Layer // ============================================================================ /** * SERVICE: graphService * PURPOSE: Handle all Microsoft Graph API calls with proper headers * DEPENDENCIES: accessToken store, error handling * * Exports: Functions for each Graph API operation * Error handling: Throws errors that UI catches */ // File: src/services/graphService.ts import { get } from 'svelte/store'; import { accessToken, refreshAccessToken, clearAuth } from '../stores/auth'; /** * Helper: Ensure token is valid before API call * * Checks if token expired, refreshes if needed * Throws if no token available */ async function ensureValidToken(): Promise { const token = get(accessToken); if (!token) { throw new Error('No access token available - not authenticated'); } // Check if token might be expired, refresh proactively const expiry = Number(sessionStorage.getItem('ms_token_expiry') || 0); if (expiry - Date.now() < 5 * 60 * 1000) { // Token expires in less than 5 minutes, refresh now await refreshAccessToken(); } return get(accessToken) || token; } /** * API CALL: Get list of files in OneDrive * * Endpoint: GET https://graph.microsoft.com/v1.0/me/drive/root/children * Scopes required: Files.Read.All * Caching: Results cached in IndexedDB via jobsStore * * Errors: * - 401: Token invalid, user redirected to login * - 403: Insufficient permissions * - 404: Folder not found * - 429: Rate limited, should retry with backoff */ export async function listFiles(folderId: string = 'root'): Promise { const token = await ensureValidToken(); const endpoint = folderId === 'root' ? 'https://graph.microsoft.com/v1.0/me/drive/root/children' : `https://graph.microsoft.com/v1.0/me/drive/items/${folderId}/children`; try { const response = await fetch(endpoint, { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, }); if (response.status === 401) { // Token invalid, clear auth and redirect clearAuth(); throw new Error('Session expired - please log in again'); } if (!response.ok) { const error = await response.json().catch(() => ({ message: response.statusText })); throw new Error(`Graph API error ${response.status}: ${error.message}`); } const data = await response.json(); return data.value || []; } catch (error) { console.error('Graph API error:', error); throw error; } } // ============================================================================ // PATTERN 4: Data Store (Derived from API) // ============================================================================ /** * STORE: jobs * PURPOSE: Cached list of jobs loaded from device storage (IndexedDB) * UPDATES: Called by services after fetching from Graph API * PERSISTENCE: IndexedDB database * * STRUCTURE: * - jobs: Job[] - Current list of jobs * - jobsLoading: boolean - Loading state * - jobsError: string | null - Last error if any */ // File: src/stores/jobs.ts import { writable } from 'svelte/store'; import { loadJobsFromDB, saveJobsToDBt } from '../services/storageService'; export const jobs = writable([]); export const jobsLoading = writable(false); export const jobsError = writable(null); /** * Load jobs from device storage * * Called on app init * Populates jobs store from IndexedDB */ export async function loadJobs(): Promise { jobsLoading.set(true); try { const cachedJobs = await loadJobsFromDB(); jobs.set(cachedJobs); jobsError.set(null); } catch (error) { console.error('Failed to load jobs:', error); jobsError.set((error as Error).message); } finally { jobsLoading.set(false); } } /** * Search jobs locally (no API call needed) * * All job data is already in device storage * This filters locally for instant results */ export async function searchJobs(query: string): Promise { // Get current jobs const allJobs = get(jobs); // Filter based on query const results = allJobs.filter(job => job.title.toLowerCase().includes(query.toLowerCase()) || job.company.toLowerCase().includes(query.toLowerCase()) || job.description.toLowerCase().includes(query.toLowerCase()) ); return results; } // ============================================================================ // PATTERN 5: Layout Component (Root) // ============================================================================ /** * COMPONENT: +layout.svelte * PURPOSE: Root layout for all pages * FEATURES: Navigation, auth check, error handling * * Lifecycle: Checks auth on mount, refreshes token */ // File: src/routes/+layout.svelte
{#if error} {/if}
// ============================================================================ // PATTERN 6: Route Page with Auth Guard // ============================================================================ /** * PAGE: +page.svelte (Home) * PURPOSE: Job listing homepage * AUTH: Redirects to login if not authenticated */ // File: src/routes/+page.svelte {#if $isAuthenticated}

Job Listings

{/if} // File: src/routes/signin/+page.svelte (OAuth login page)
// ============================================================================ // SUMMARY // ============================================================================ /** * SVELTE PATTERNS - GOLDEN RULES: * * 1. STORES: Centralize state, update via explicit functions * 2. COMPONENTS: Keep reactive, let stores handle state * 3. SERVICES: Handle API calls with error handling, never expose tokens * 4. LIFECYCLE: Use onMount for async operations * 5. DERIVED: Use derived stores for computed state (isAuthenticated) * 6. REACTIVITY: Use $store syntax to subscribe in templates * 7. PROPS: Always document and type check * 8. ERROR HANDLING: Catch all async operations, display to user * 9. PERSISTENCE: Use IndexedDB for device storage, sessionStorage for tokens * 10. TESTING: Every component independently testable */