diff --git a/src/lib/stores/notes.ts b/src/lib/stores/notes.ts index a5d17d4..763022f 100644 --- a/src/lib/stores/notes.ts +++ b/src/lib/stores/notes.ts @@ -1,6 +1,6 @@ import { writable, derived } from 'svelte/store'; import type { Note, NoteFilter, NoteCreate, NoteUpdate } from '$lib/types/index.js'; -import { addToSyncQueue, putNote, deleteNoteFromIdb, getAllNotes } from '$lib/sync/idb.js'; +import { addToSyncQueue, putNote, deleteNoteFromIdb, getAllNotes, clearNotes as clearIdbNotes } from '$lib/sync/idb.js'; import { showToast } from '$lib/stores/toast.js'; import { extractTags } from '$lib/utils/tags.js'; @@ -45,21 +45,27 @@ function isNetworkError(err: unknown): boolean { } export async function loadNotes(filter: NoteFilter = 'all') { + // Load from IDB first for instant display + const cached = await getAllNotes(); + if (cached.length > 0) { + notes.set(cached); + currentFilter.set(filter); + } + + // Then fetch from server in the background try { const res = await fetch(`/api/notes?filter=${filter}`); if (res.ok) { - const data = await res.json(); + const data: Note[] = await res.json(); notes.set(data); currentFilter.set(filter); + + // Update IDB with server state + await clearIdbNotes(); + await Promise.all(data.map((n) => putNote(n))); } - } catch (err) { - if (isNetworkError(err)) { - const cached = await getAllNotes(); - if (cached.length > 0) { - notes.set(cached); - currentFilter.set(filter); - } - } + } catch { + // Offline — IDB data already loaded above } } @@ -71,8 +77,9 @@ export async function createNote(note: NoteCreate): Promise { body: JSON.stringify(note) }); if (res.ok) { - const created = await res.json(); + const created: Note = await res.json(); notes.update((list) => [created, ...list]); + await putNote(created); return created; } return null; @@ -115,8 +122,9 @@ export async function updateNote(id: string, updates: NoteUpdate): Promise list.map((n) => (n.id === id ? updated : n))); + await putNote(updated); return updated; } showToast('Failed to save note', 'error'); @@ -152,6 +160,7 @@ export async function deleteNote(id: string): Promise { const res = await fetch(`/api/notes/${id}`, { method: 'DELETE' }); if (res.ok) { notes.update((list) => list.filter((n) => n.id !== id)); + await deleteNoteFromIdb(id); return true; } return false; diff --git a/src/lib/sync/client.ts b/src/lib/sync/client.ts index 3c80cb8..e3dfe63 100644 --- a/src/lib/sync/client.ts +++ b/src/lib/sync/client.ts @@ -2,7 +2,7 @@ import { browser } from '$app/environment'; import type { Note } from '$lib/types/index.js'; import { putNote, getAllNotes, addToSyncQueue, getSyncQueue, clearSyncQueue, deleteNoteFromIdb, getMeta, setMeta } from './idb.js'; import { mergeNotes } from './crdt.js'; -import { notes as notesStore, currentFilter } from '$lib/stores/notes.js'; +import { loadNotes, currentFilter } from '$lib/stores/notes.js'; import { get } from 'svelte/store'; export type SyncStatus = 'synced' | 'syncing' | 'offline' | 'error'; @@ -101,13 +101,8 @@ export async function sync(): Promise { await pullChanges(); setStatus('synced'); - // Reload notes from server to reflect synced state - const filter = get(currentFilter); - const res = await fetch(`/api/notes?filter=${filter}`); - if (res.ok) { - const data = await res.json(); - notesStore.set(data); - } + // Reload notes from server and persist to IDB + await loadNotes(get(currentFilter)); } catch { setStatus('error'); } diff --git a/tests/e2e/offline.spec.ts b/tests/e2e/offline.spec.ts index 8f17529..7667e55 100644 --- a/tests/e2e/offline.spec.ts +++ b/tests/e2e/offline.spec.ts @@ -79,6 +79,23 @@ test.describe('Offline Support', () => { await expect(page.getByText('Survive Reload')).toBeVisible(); }); + test('Scenario: Notes load from local cache when the API is unavailable', async ({ authenticatedPage: page }) => { + // Given a note titled "Cached Note" exists + await page.getByTestId('new-note-btn').click(); + await page.getByTestId('note-title-input').fill('Cached Note'); + await page.getByTestId('close-editor-btn').click(); + await expect(page.getByText('Cached Note')).toBeVisible(); + + // When the API is unavailable (simulating service worker serving cached shell) + await page.route('**/api/**', (route) => route.abort()); + + // And the user reloads the page + await page.reload(); + + // Then the note is still visible (loaded from IDB cache) + await expect(page.getByText('Cached Note')).toBeVisible(); + }); + test('Scenario: Trashed note disappears from the list when offline', async ({ authenticatedPage: page }) => { // Given a note titled "Offline Trash" exists await page.getByTestId('new-note-btn').click();