jedmund-svelte/docs/autosave-completion-guide.md
Justin Edmund 39e82146d9 docs: update autosave completion guide with new API
Added implementation summary showing:
- All 5 forms now use runes-based autosave
- New reactive API without subscriptions
- Key improvements (prime, auto-idle, smart guards)
- Marked as completed January 2025

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 16:16:32 -07:00

6.2 KiB

Admin Autosave Completion Guide

Status: COMPLETED (January 2025)

All objectives have been achieved. This document is preserved for historical reference and implementation details.

Implementation Summary

All admin forms now use the modernized runes-based autosave system (createAutoSaveStore):

  • ProjectForm - Migrated to runes with full lifecycle management
  • Posts Editor - Migrated with draft recovery banner
  • EssayForm - Added autosave from scratch
  • PhotoPostForm - Added autosave from scratch
  • SimplePostForm - Added autosave from scratch

New API (Svelte 5 Runes)

import { createAutoSaveStore } from '$lib/admin/autoSave.svelte'

const autoSave = createAutoSaveStore({
  debounceMs: 2000,
  idleResetMs: 2000,
  getPayload: () => buildPayload(),
  save: async (payload, { signal }) => {
    const response = await fetch('/api/endpoint', {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
      credentials: 'same-origin',
      signal
    })
    if (!response.ok) throw new Error('Failed to save')
    return await response.json()
  },
  onSaved: (saved, { prime }) => {
    updatedAt = saved.updatedAt
    prime(buildPayload())
    clearDraft(draftKey)
  }
})

// Reactive state - no subscriptions needed!
autoSave.status  // 'idle' | 'saving' | 'saved' | 'error' | 'offline'
autoSave.lastError  // string | null

Key Improvements

  1. No autosaves on load: prime() sets initial baseline
  2. Auto-idle transition: Status automatically resets to 'idle' after save
  3. Smart navigation guards: Only block if unsaved changes exist
  4. Draft-on-failure: localStorage only used when autosave fails
  5. Proper cleanup: destroy() called on unmount
  6. Reactive API: Direct property access instead of subscriptions

Original Objectives

  • Eliminate redundant save requests triggered on initial page load.
  • Restore reliable local draft recovery, including clear-up of stale backups.
  • Deliver autosave status feedback that visibly transitions back to idle after successful saves.
  • Ensure navigation/unload flows wait for pending autosaves instead of cancelling them mid-flight.

Key Problem Areas

Missing Draft Handlers

  • src/routes/admin/posts/[id]/edit/+page.svelte:425 references restoreDraft and dismissDraft, but the functions are never defined. Draft recovery buttons therefore break compilation and runtime behavior.

Immediate Autosaves on Load

  • Effects in src/routes/admin/posts/[id]/edit/+page.svelte:307 and src/lib/components/admin/ProjectForm.svelte:157 call autoSave.schedule() as soon as the component mounts. Because the payload hash includes updatedAt, each mount triggers redundant PUTs until the server response realigns the hash.

Ineffective Navigation Guard

  • beforeNavigate(() => autoSave.flush()) (posts + project form) does not cancel the outbound navigation, so the flush typically aborts when the route unloads. Result: unsaved work if the user navigates away during a pending autosave.

Controller Lifecycle Gaps

  • createAutoSaveController timers/AbortController persist after leaving the page because callers never invoke destroy().
  • Post editor imports clearDraft but never clears the draft after successful saves or when dismissing the prompt, so stale backups reappear.

Controller Enhancements (src/lib/admin/autoSave.ts)

  • Baseline priming: Add a prime(initialPayload) (or allow onSaved to pass the response payload) to set lastSentHash immediately after fetching server data. This prevents an automatic save when the user has not made changes.
  • Auto-idle transition: When status becomes 'saved', set a timeout (e.g., 2s) that reverts status to 'idle'. Cancel the timeout on any new state change.
  • Robust destroy: Ensure destroy() clears pending timers and aborts the current request; expose and require callers to invoke it on component teardown.
  • Consider optional helper flags (e.g., autoResetStatus) so forms do not reimplement timing logic.

Shared Lifecycle Helper

Create a utility (e.g., initAutoSaveLifecycle) that accepts the controller plus configuration:

  • Registers keyboard shortcut (Cmd/Ctrl+S) to flush() once the page has loaded.
  • Provides a real navigation guard that cancels the navigation event, awaits flush(), then resumes or surfaces an error.
  • Hooks into onDestroy to remove listeners and call controller.destroy().
  • Optionally wires window unload handling if needed.

Form Integration Checklist

Posts Editor (src/routes/admin/posts/[id]/edit/+page.svelte)

  1. Implement restoreDraft / dismissDraft and handle clearDraft after autosave or manual save success.
  2. Introduce a hasLoaded flag set after loadPost() (and controller prime) before scheduling autosave.
  3. Adopt the shared lifecycle helper for navigation, keyboard shortcuts, and cleanup.

Project Form (src/lib/components/admin/ProjectForm.svelte)

  1. Mirror baseline priming and hasLoaded gating before scheduling.
  2. Clear drafts on success or dismissal, and reuse the lifecycle helper.
  3. Ensure autosave only starts after the initial project data populates formData.

Other Forms (Simple Post, Essay, Photo, etc.)

  • Audit each admin form to ensure they use the shared lifecycle helper, seed baselines, clear drafts, and transition status back to idle.

Testing & Verification

  • Unit Tests: Cover controller state transitions, baseline priming, abort handling, and auto-idle timeout (tests/autoSaveController.test.ts). Run with node --test --loader tsx tests/autoSaveController.test.ts.
  • Component Tests: Verify autosave does not fire on initial mount, drafts restore/clear correctly, and navigation waits for flush.
  • Manual QA: Confirm keyboard shortcut behavior, offline fallback, and that UI returns to idle after showing “saved”.

Structural Considerations

  • Factor shared autosave wiring into reusable modules to avoid copy/paste drift.
  • Ensure server response payloads used in prime() reflect the canonical representation (including normalized fields) so hashes stay in sync.
  • Document the lifecycle helper so new admin screens adopt the proven pattern without regression.