# Execution Plan: Fix SSR Violations and SvelteKit Best Practices ## Outcome Name FixEventSourceSSRAndBestPractices ## Problem Analysis ### Primary Issue `ReferenceError: EventSource is not defined` at `/home/moze/Sources/insta-recipe/src/routes/+page.svelte:299:66` ### Root Cause The code is accessing `EventSource` during server-side rendering (SSR), but `EventSource` is a browser-only Web API that doesn't exist in Node.js. Additionally, comprehensive codebase analysis revealed multiple SSR violations and SvelteKit anti-patterns. ### Affected Files - Critical 1. **[src/routes/+page.svelte](src/routes/+page.svelte)** - EventSource accessed at L299, L82 without browser guards 2. **[src/routes/share/+page.svelte](src/routes/share/+page.svelte#L22-L25)** - `$effect` with side effects (calls `process()` function) 3. **[src/routes/share/components/LlmHealthIndicator.svelte](src/routes/share/components/LlmHealthIndicator.svelte#L36-L39)** - `$effect` with `setInterval` (no browser guard) ### Affected Files - Already Compliant (Good Examples) 1. **[src/lib/client/PushNotificationManager.ts](src/lib/client/PushNotificationManager.ts)** ✅ - Properly uses `browser` from `$app/environment` (L10) - Guards `localStorage` access (L296, L300) - Guards `window.atob` access (L318) - Guards `navigator.serviceWorker` access (L111) 2. **[src/lib/client/ServiceWorkerMessageHandler.ts](src/lib/client/ServiceWorkerMessageHandler.ts)** ✅ - All browser APIs properly used in client-only context - Not imported/used in SSR contexts ### SvelteKit Best Practices (from llms-full.txt documentation) #### 1. Browser API Access **Pattern:** Import `browser` from `$app/environment` and guard all browser-only APIs ```js import { browser } from '$app/environment'; if (browser) { // Browser-only code } ``` **Browser-only APIs to guard:** - `window.*` - `document.*` - `localStorage`, `sessionStorage` - `navigator.*` - `EventSource`, `WebSocket` - `location.*` #### 2. Lifecycle Hooks **Pattern:** `onMount` only runs in browser (built-in SSR guard) ```js import { onMount } from 'svelte'; onMount(() => { // Automatically browser-only // Still good practice to add explicit browser check for clarity }); ``` #### 3. Runes and Reactivity **`$effect` gotcha:** Effects run during SSR AND hydration. Must guard browser APIs! ```js $effect(() => { if (!browser) return; // Browser-only reactive code }); ``` **`$derived` gotcha:** Computed values run during SSR. Keep them pure! ```js // ✅ GOOD - pure computation let doubled = $derived(count * 2); // ❌ BAD - side effects in derived let value = $derived(localStorage.getItem('key')); // SSR crash! ``` #### 4. State Initialization **Pattern:** Initialize with SSR-safe defaults, update in `onMount` ```js let data = $state(null); onMount(() => { // Load browser-only data data = JSON.parse(localStorage.getItem('key') || 'null'); }); ``` #### 5. Static Constants **Gotcha:** Accessing static properties of browser APIs causes SSR errors ```js // ❌ BAD - EventSource.OPEN doesn't exist in Node if (eventSource?.readyState === EventSource.OPEN) // ✅ GOOD - Use numeric constants or guard if (browser && eventSource?.readyState === EventSource.OPEN) // OR if (eventSource?.readyState === 1) // EventSource.OPEN = 1 ``` ### Codebase Analysis Results #### ✅ Already Properly Guarded - `PushNotificationManager.ts` - Excellent example of SSR-safe patterns - `ServiceWorkerMessageHandler.ts` - Client-only, properly scoped - All API routes in `src/routes/api/**` - Server-only contexts - Service worker (`service-worker.ts`) - Runs in worker context only #### ⚠️ Needs Fixing **High Priority (Breaking SSR):** 1. **+page.svelte (Queue Dashboard)** - L299: `eventSource?.readyState === EventSource.OPEN` - No browser guard - L82: `eventSource?.readyState === EventSource.CLOSED` - No browser guard - L67: `new EventSource()` - Inside `onMount` but needs explicit guard - Missing `browser` import 2. **LlmHealthIndicator.svelte** - L36-39: `$effect` with `setInterval` - No browser guard - Should use `onMount` instead for timer setup **Medium Priority (Anti-patterns):** 3. **share/+page.svelte** - L22-25: `$effect` calling `process()` with side effects - Should use `onMount` with conditional logic instead - `$effect` is meant for synchronization, not side effects #### 📋 Not Issues (Clarifications) - `setTimeout` in components (L81 in +page.svelte, L53 in share/+page.svelte) - ✅ OK because inside `onMount` or event handlers - `goto` from `$app/navigation` - ✅ SSR-safe (SvelteKit handles this) - `$page` store from `$app/stores` - ✅ SSR-safe (available in both contexts) - Server-side code (`lib/server/**`) using browser automation - ✅ OK (different context, uses Puppeteer) ## Stories ## Stories ### Story 1: Fix EventSource SSR in Queue Dashboard **As a** developer **I want** to guard all EventSource usage from SSR execution **So that** the application doesn't crash with "EventSource is not defined" **Acceptance Criteria:** - Import `browser` from `$app/environment` - Guard `EventSource` constructor in `startSSEConnection()` - Replace `EventSource.OPEN` constant with numeric value `1` or add browser guard - Replace `EventSource.CLOSED` constant with numeric value `2` or add browser guard - Connection status works correctly after hydration - No SSR errors in server logs **Technical Details:** **Lines to fix:** - L2: Add `import { browser } from '$app/environment';` - L67-68: Add browser guard before creating EventSource - L82: Change `EventSource.CLOSED` to `2` or guard with `browser` - L299: Change `EventSource.OPEN` to `1` or guard with `browser` **Implementation:** ```typescript import { browser } from '$app/environment'; function startSSEConnection() { if (!browser) return; // ✅ Guard try { eventSource = new EventSource('/api/queue/stream'); // ... rest } } // In reconnection logic (L82): if (browser && eventSource?.readyState === 2) { // CLOSED = 2 startSSEConnection(); } // In template (L299):
``` **Files:** - [src/routes/+page.svelte](src/routes/+page.svelte) --- ### Story 2: Fix $effect Anti-pattern in Share Page **As a** developer **I want** to replace `$effect` side effects with `onMount` pattern **So that** the code follows SvelteKit best practices **Acceptance Criteria:** - Replace `$effect` with `onMount` for auto-processing shared URLs - No side effects in reactive expressions - Auto-processing still works when URL is shared - No unnecessary re-triggering **Technical Details:** According to SvelteKit documentation, `$effect` should be used for synchronization, not side effects like API calls. Use `onMount` instead. **Current problematic code (L22-25):** ```typescript $effect(() => { if (targetUrl && status === 'idle') { process(); } }); ``` **Fixed code:** ```typescript let hasProcessed = $state(false); onMount(() => { if (targetUrl && !hasProcessed) { hasProcessed = true; process(); } }); ``` **Files:** - [src/routes/share/+page.svelte](src/routes/share/+page.svelte) **SvelteKit Pattern Reference:** > Use `$effect` for synchronizing derived state, DOM manipulation, or reactive cleanup. > Use `onMount` for initialization, API calls, and browser-only setup. --- ### Story 3: Fix setInterval SSR in LLM Health Indicator **As a** developer **I want** to guard `setInterval` from SSR execution **So that** the component doesn't break during server rendering **Acceptance Criteria:** - Add browser guard to `$effect` containing `setInterval` - Health polling only runs in browser - Component renders safely during SSR - Cleanup still works correctly **Technical Details:** **Current code (L36-39):** ```typescript $effect(() => { checkHealth(); // Initial check const interval = setInterval(checkHealth, pollInterval); return () => clearInterval(interval); }); ``` **Fixed code:** ```typescript $effect(() => { if (!browser) return; // ✅ SSR guard checkHealth(); // Initial check const interval = setInterval(checkHealth, pollInterval); return () => clearInterval(interval); }); ``` **Better alternative - use onMount:** ```typescript onMount(() => { checkHealth(); // Initial check const interval = setInterval(checkHealth, pollInterval); return () => clearInterval(interval); }); ``` **Files:** - [src/routes/share/components/LlmHealthIndicator.svelte](src/routes/share/components/LlmHealthIndicator.svelte) --- ### Story 4: Add SSR Best Practices Documentation **As a** developer **I want** documentation on SSR best practices for this project **So that** future development avoids these issues **Acceptance Criteria:** - Create or update developer documentation - Include examples from the codebase - Reference SvelteKit official documentation - Add inline comments explaining SSR guards **Technical Details:** Create documentation covering: 1. Browser API detection with `$app/environment` 2. Lifecycle hook usage (`onMount` vs `$effect`) 3. Common gotchas (static constants, timers, storage APIs) 4. Good examples from our codebase (PushNotificationManager) **Files to create/update:** - `docs/SVELTEKIT_SSR_GUIDE.md` (new) - Add inline comments to fixed files --- ### Story 5: Comprehensive SSR Audit and Testing **As a** developer **I want** to verify no other SSR violations exist **So that** the application is fully SSR-safe **Acceptance Criteria:** - Manual SSR test: `npm run build && npm run preview` - Check server logs for any SSR errors - Test all routes with JavaScript disabled (progressive enhancement) - Verify hydration works correctly - No console warnings about hydration mismatches **Technical Details:** **Testing checklist:** - [ ] Build production bundle: `npm run build` - [ ] Preview production: `npm run preview` - [ ] Navigate to all routes - [ ] Check server console for errors - [ ] Verify SSE connection works - [ ] Test push notification UI - [ ] Test queue dashboard - [ ] Test share page with/without URL params **Search patterns to verify:** ```bash # Find any unguarded browser API usage grep -r "window\." src/routes --include="*.svelte" grep -r "document\." src/routes --include="*.svelte" grep -r "localStorage" src/routes --include="*.svelte" grep -r "navigator\." src/routes --include="*.svelte" ``` **Known safe patterns:** - API routes (`src/routes/api/**`) - server-only - Client libraries (`src/lib/client/**`) - properly guarded - Event handlers (`onclick`, `onsubmit`) - run client-side - `onMount` callbacks - run client-side ## Implementation Plan ### Phase 1: Critical Fixes (Blocks Production) **Priority:** URGENT - Fixes SSR crashes 1. **Story 1** - Fix EventSource in Queue Dashboard - Add `browser` import - Guard EventSource creation - Fix static constant references - Test SSR rendering 2. **Story 3** - Fix setInterval in LLM Health Indicator - Add browser guard to $effect OR convert to onMount - Test component SSR **Estimated Time:** 30 minutes **Testing:** Build and preview, check server logs --- ### Phase 2: Best Practices (Improves Code Quality) **Priority:** HIGH - Fixes anti-patterns 3. **Story 2** - Fix $effect anti-pattern in Share Page - Replace $effect with onMount - Add processed flag to prevent re-runs - Test auto-processing behavior **Estimated Time:** 20 minutes **Testing:** Test share target flow --- ### Phase 3: Validation & Documentation (Prevents Future Issues) **Priority:** MEDIUM - Long-term maintainability 4. **Story 5** - Comprehensive SSR Audit - Run production build - Test all routes - Verify no SSR errors 5. **Story 4** - Documentation - Create SSR best practices guide - Add inline comments - Document patterns from PushNotificationManager **Estimated Time:** 1 hour **Testing:** Full regression test --- ### Total Estimated Time - Critical fixes: 30 min - Best practices: 20 min - Validation & docs: 1 hour - **Total: ~2 hours** ## Technical Specifications ### SvelteKit Runes Reference #### `$state` - Reactive State ```typescript let count = $state(0); // Simple state let obj = $state({ name: 'Alice' }); // Deep reactive proxy ``` - ✅ SSR-safe for primitive values - ⚠️ Don't initialize with browser APIs #### `$derived` - Computed Values ```typescript let doubled = $derived(count * 2); ``` - ✅ Runs during SSR - ⚠️ Must be pure (no side effects) - ❌ Don't access browser APIs #### `$effect` - Reactive Side Effects ```typescript $effect(() => { // Runs during SSR AND hydration console.log('count changed:', count); }); ``` - ⚠️ Runs in both SSR and browser - ✅ Use for synchronization - ❌ Not for initialization or API calls - **Must guard browser APIs** #### `onMount` - Browser-Only Lifecycle ```typescript onMount(() => { // Only runs in browser return () => { // Cleanup }; }); ``` - ✅ Only runs in browser - ✅ Use for initialization - ✅ Use for browser API access ### Browser API Constants Some browser APIs expose static constants that don't exist during SSR: **EventSource:** - `EventSource.CONNECTING = 0` - `EventSource.OPEN = 1` - `EventSource.CLOSED = 2` **Solutions:** ```typescript // ❌ BAD - Crashes SSR if (es.readyState === EventSource.OPEN) // ✅ GOOD - Use numeric value if (es.readyState === 1) // ✅ GOOD - Guard access if (browser && es.readyState === EventSource.OPEN) ``` **WebSocket:** Similar issue with `WebSocket.OPEN`, etc. ### Dependencies - `$app/environment` - Built-in SvelteKit module - No new package dependencies required ### Files to Modify **Critical (Phase 1):** 1. `src/routes/+page.svelte` - Queue dashboard 2. `src/routes/share/components/LlmHealthIndicator.svelte` - Health indicator **Best Practices (Phase 2):** 3. `src/routes/share/+page.svelte` - Share page **Documentation (Phase 3):** 4. `docs/SVELTEKIT_SSR_GUIDE.md` - New file ### Code Patterns Summary #### Pattern 1: Browser API in Component State ```svelte
Status: {eventSource?.readyState === 1 ? 'Connected' : 'Disconnected'}
``` #### Pattern 2: Timers and Intervals ```svelte ``` #### Pattern 3: Auto-Processing on Mount ```svelte ``` ## Risk Assessment ### High Priority Risks - **SSR/Hydration mismatch**: If guards are inconsistent between server and client - **Mitigation**: Use numeric constants; avoid conditional rendering based on `browser` - **Testing**: Check for hydration warnings in console ### Medium Priority Risks - **Regression in auto-processing**: Share page might not auto-process URLs - **Mitigation**: Thorough testing of share target flow - **Testing**: Test with Instagram share and manual URL input - **Connection status flicker**: Status indicator might show wrong state briefly - **Mitigation**: Initialize with sensible defaults - **Testing**: Watch for visual flicker on page load ### Low Priority Risks - **Performance**: Minimal, browser checks are fast - **Breaking changes**: Unlikely, only fixing internal implementation ## Testing Strategy ### Unit Testing - Not applicable - these are integration-level fixes - Existing tests should continue to pass ### Integration Testing **Manual testing required:** 1. **SSR Testing:** ```bash npm run build npm run preview # Check server console for errors # Navigate to all pages ``` 2. **EventSource Connection:** - Open queue dashboard - Check browser DevTools → Network → EventSource - Verify "Live updates" status indicator - Add queue item and verify real-time update 3. **Share Page:** - Navigate to `/share` - Manually enter URL → should work - Share from Instagram → should auto-process - Check no duplicate processing 4. **LLM Health:** - Check health indicator appears - Verify polling happens (check Network tab) - No SSR errors in console ### Edge Cases - **Server restart** while client connected → Reconnection works - **Network disconnection** → Graceful degradation - **JavaScript disabled** → Progressive enhancement (no errors) - **Multiple tabs** open → Each maintains own connection ### Hydration Testing - Disable JavaScript after SSR - Enable JavaScript and check hydration - Look for console warnings: - "Hydration failed" - "The server response doesn't match the client content" ### Browser Compatibility - Modern browsers with EventSource support - Browsers without EventSource → Should show disconnected status (no crash) ## Success Metrics ### Must Have (Phase 1) 1. ✅ No `EventSource is not defined` errors 2. ✅ No `setInterval is not defined` errors 3. ✅ Production build succeeds 4. ✅ SSR renders without errors 5. ✅ Live updates work in browser ### Should Have (Phase 2) 6. ✅ No `$effect` anti-patterns 7. ✅ No hydration warnings 8. ✅ Share page auto-processing works ### Nice to Have (Phase 3) 9. ✅ SSR best practices documentation 10. ✅ Inline comments explaining patterns 11. ✅ All routes tested and verified ## Anti-Patterns to Avoid ### ❌ Don't Do This ```typescript // 1. Browser API in $derived let data = $derived(localStorage.getItem('key')); // SSR crash! // 2. Side effects in $effect without guard $effect(() => { fetch('/api/data'); // Runs during SSR! }); // 3. Static constants without guard if (ws.readyState === WebSocket.OPEN) // SSR crash! // 4. Initialization in $effect $effect(() => { // Use onMount instead initialize(); }); ``` ### ✅ Do This Instead ```typescript // 1. Load in onMount let data = $state(null); onMount(() => { data = localStorage.getItem('key'); }); // 2. Guard browser APIs in $effect $effect(() => { if (!browser) return; fetch('/api/data'); }); // 3. Use numeric constants or guard if (ws.readyState === 1) // WebSocket.OPEN = 1 // OR if (browser && ws.readyState === WebSocket.OPEN) // 4. Initialize in onMount onMount(() => { initialize(); }); ``` ## References ### Official Documentation - [SvelteKit SSR](https://svelte.dev/llms-full.txt) - From llms-full.txt - [Svelte Runes](https://svelte.dev/llms-full.txt) - $state, $derived, $effect - [SvelteKit $app modules](https://svelte.dev/llms-full.txt) - $app/environment, $app/stores ### Our Codebase Examples - **Good:** [PushNotificationManager.ts](src/lib/client/PushNotificationManager.ts) - Excellent SSR-safe patterns - **Good:** [ServiceWorkerMessageHandler.ts](src/lib/client/ServiceWorkerMessageHandler.ts) - Client-only scope - **Fix:** [+page.svelte](src/routes/+page.svelte) - EventSource needs guards - **Fix:** [LlmHealthIndicator.svelte](src/routes/share/components/LlmHealthIndicator.svelte) - setInterval needs guard - **Improve:** [share/+page.svelte](src/routes/share/+page.svelte) - $effect anti-pattern ### Web APIs - [EventSource MDN](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) - [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) ## Appendix: Complete Code Changes ### A. +page.svelte (Queue Dashboard) **Before:** ```svelte
``` **After:** ```svelte
``` ### B. LlmHealthIndicator.svelte **Before:** ```svelte ``` **After (Option 1 - Guard $effect):** ```svelte ``` **After (Option 2 - Use onMount - RECOMMENDED):** ```svelte ``` ### C. share/+page.svelte **Before:** ```svelte ``` **After:** ```svelte ``` --- **Plan Complete - Ready for Implementation**