feat: persist starred sessions in SQLite database#94
Conversation
Migrate starring from localStorage to a server-side starred_sessions table. The frontend store now loads from the API on startup, uses optimistic updates for instant UI feedback, and automatically migrates any existing localStorage stars to the database on first load. Closes wesm#93 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roborev: Combined Review (
|
Address review feedback: - Replace raw err.Error() in all starred handlers with generic "internal error" message; log details server-side only - Fix race between initial load() and optimistic star/unstar: merge server response with any pending local changes instead of blind replacement, and deduplicate concurrent load() calls Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roborev: Combined Review (
|
BulkStarSessions now uses INSERT ... SELECT ... WHERE EXISTS to silently skip session IDs that no longer exist in the sessions table (e.g. pruned or deleted from disk). Previously, INSERT OR IGNORE did not suppress foreign key violations, causing the entire migration transaction to fail when localStorage contained any stale ID. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Addressed remaining roborev feedback in
Previously addressed in
|
roborev: Combined Review (
|
…back races - Remove starred_sessions from needsSchemaRebuild probes — table uses CREATE TABLE IF NOT EXISTS, so probing for it on pre-feature DBs triggers an unnecessary full schema rebuild (data loss) - After bulk migration, refresh from listStarred() instead of blindly adding toMigrate IDs (backend silently skips stale IDs) - Add per-session version tokens to star/unstar — stale error callbacks no longer revert newer user actions on rapid toggle Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9a14eb2 to
0522e22
Compare
|
Addressed in
|
roborev: Combined Review (
|
… on load failure Merge migrated IDs into current state instead of replacing, preventing optimistic updates from being clobbered. Keep loaded=false on fallback so transient backend failures don't permanently skip server reload. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roborev: Combined Review (
|
loadVersion counter increments on every star/unstar, so doLoad discards server responses that arrive after the user changed state. Migration errors now propagate to doLoad's catch block, keeping loaded=false so the next load() retries instead of short-circuiting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When loadVersion changes during doLoad, skip merging the stale server response but still complete migration and set loaded=true. Prevents the store from getting stuck in an unloaded state when the user acts during the initial fetch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roborev: Combined Review (
|
When a star/unstar operation happened during the initial load, the server response was completely discarded, losing pre-existing DB stars. Now always merge server IDs into local state, skipping only IDs the user explicitly toggled (tracked by opVersions) since those are already reflected optimistically. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roborev: Combined Review (
|
Replaces the fragile opVersions-based load blocking with a clean mutationVersion counter pattern (same as the pin store): - mutationVersion incremented on every star/unstar; load and migration capture it before async ops and discard stale responses - opVersions now only used for rollback safety in .catch handlers, not as a permanent blocklist in load reconciliation - localStorage fallback guarded by mutationVersion (not ids.size === 0) so user mutations during a failed load are preserved - loaded flag set after successful fetch, before migration, so migration failure doesn't block future server retries - Migration refresh guarded by its own mutationVersion snapshot Fixes all 3 issues from roborev review cycles: 1. Stale migration refresh overwriting optimistic state 2. opVersions acting as permanent blocklist in load path 3. Load response clobbering user actions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roborev: Combined Review (
|
…n error Migration: clearLocalStorage now only runs when the server refresh was actually applied to in-memory state (mutVer matches). When a mutation happened during migration, localStorage is preserved for retry on next page load instead of being prematurely cleared. Error handling: Replace per-session opVersions rollback with reconcileAfterError that fetches authoritative server state. This eliminates incorrect rollback from rapid star/unstar where both API calls fail — server truth is always restored. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roborev: Combined Review (
|
HIGH: Rapid star/unstar on the same session could arrive at the server out of order, leaving the DB in the wrong state. Fix: enqueue mutations per session via promise chains so they always execute in user-intent order on the server. MEDIUM: reconcileAfterError could overwrite optimistic state from another still-in-flight mutation. Fix: reconcileIfIdle only fires when all queues have drained (queues.size === 0). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roborev: Combined Review (
|
Multiple listStarred() calls (from doLoad, migrateLocalStorage, and reconcileIfIdle) could be in-flight under the same mutationVersion. Add a monotonic refreshId counter — only the latest response applies. This prevents an older response from overwriting a newer one when migration and reconciliation race. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roborev: Combined Review (
|
When migrateLocalStorage fails (bulkStar or listStarred error), merge the to-migrate IDs into the in-memory set so they remain visible in the UI for the rest of the session. localStorage is preserved for retry on next page reload. Previously, migration failure left local stars invisible because loaded=true prevented reload and localStorage wasn't cleared. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roborev: Combined Review (
|
Move clearLocalStorage() to run immediately after bulkStarSessions succeeds, before the listStarred refresh. This prevents stale local IDs from being re-migrated on a later reload if the user unstars them between sessions. Separate try/catch blocks for bulkStar and listStarred so each failure path handles localStorage and in-memory state correctly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roborev: Combined Review (
|
…ping When listStarred() fails during initial load and mutations occurred concurrently (mutVer mismatch), the localStorage fallback was skipped entirely, losing legacy starred IDs. Now merges local stars into current optimistic state when stale, preserving both mutation state and legacy IDs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When merging localStorage into optimistic state on stale load failure, skip IDs that have in-flight mutations (present in queues) to avoid resurrecting sessions the user explicitly unstarred during load. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roborev: Combined Review (
|
handleStarSession previously did GetSession then StarSession in separate DB operations. If the session was deleted between calls, INSERT could fail with FK error returning 500. StarSession now uses INSERT...SELECT WHERE EXISTS (matching BulkStarSessions pattern) and returns (bool, error) so the handler can distinguish "session not found" (404) without a separate lookup. Also adds unit tests for StarSession, UnstarSession, ListStarred, and BulkStarSessions including non-existent session edge cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
golangci-lint flagged the assignment to ok as ineffectual since it was never read. Now asserts ok is false on re-star to verify INSERT OR IGNORE no-op behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roborev: Combined Review (
|
StarSession now disambiguates "already starred" from "session not found" by doing a SELECT EXISTS check when RowsAffected is 0. This makes re-star requests return 204 (idempotent success) instead of incorrectly returning 404. unstar() now mirrors removals into localStorage via removeFromLocalStorage() so that a migration retry won't re-star sessions the user explicitly unstarred during the migration window. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roborev: Combined Review (
|
load() set loaded=true before migrateLocalStorage() finished, allowing concurrent callers to see partially-initialized state. Move loaded=true into a try/finally after the migration await so it is set only after migration completes (or fails), while still staying false if the initial listStarred() call fails. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roborev: Combined Review (
|
|
I can work on viewing and merging this? let me know when you feel like you're at a stopping point |
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
Seed ids from localStorage at construction so legacy stars are visible immediately, before the async server load and migration complete. Previously the store started empty, and a toggle during the load window could invert the intended action. Add retry with exponential backoff (2s, 4s, 8s, max 3 retries) when the initial listStarred request fails. Without retry, a transient startup failure left DB-backed stars invisible for the whole page session. Export createStarredStore factory for test isolation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Check retryTimer before scheduling so a second load() failure while a retry is already pending does not create a duplicate timer. Extract cancelRetry() to clear the timer and reset the count on successful load. Add test for repeated load() calls during a pending retry to verify only one retry chain runs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the loose toBeLessThanOrEqual assertion with exact callCount checks at each timer tick. The test now asserts callCount === 3 at t=4s (where a duplicate timer would have fired with the old code) and callCount === 4 only at t=6s (the correct 4s backoff from the single retry chain). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4dd3b20 to
fe395ac
Compare
roborev: Combined Review (
|
On refresh failure after successful bulk migration, the store was merging all toMigrate IDs back into memory. Since the server silently skips non-existent session IDs during bulk star, this re-introduced stale IDs as phantom stars for the rest of the page session. Replace the blind merge with a reconcileIfIdle() call so the correct server state is fetched once connectivity recovers. Stale IDs are never surfaced in the UI. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the single fire-and-forget reconcileIfIdle() call on migration refresh failure with a dedicated scheduleReconcile() that retries with exponential backoff (2s, 4s, 8s, max 3 attempts). Without retry, if connectivity was still down when the immediate reconcile fired, legitimately migrated IDs stayed invisible for the rest of the page session. Add test for the "fails twice, recovers on third" scenario to verify migrated IDs eventually appear without user interaction. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
roborev: Combined Review (
|
|
thanks! |
Summary
starred_sessionstable to SQLite schema for server-side star persistenceGET /starred,PUT /sessions/{id}/star,DELETE /sessions/{id}/star,POST /starred/bulkStarredStoreto load from API with optimistic updatesCloses #93
Changes
internal/db/schema.sqlstarred_sessionstableinternal/db/db.gointernal/db/starred.goStarSession,UnstarSession,ListStarredSessionIDs,BulkStarSessionsinternal/server/starred.gointernal/server/server.gofrontend/src/lib/api/client.tslistStarred,starSession,unstarSession,bulkStarSessionsfrontend/src/lib/stores/starred.svelte.tsfrontend/src/App.sveltestarred.load()on mountTest plan
go vet ./...— cleango test ./...— all passnpm run build— clean🤖 Generated with Claude Code