Skip to content

feat: save system error handling & edge cases#122

Merged
AshDevFr merged 1 commit intomainfrom
feature/save-system-error-handling
Mar 14, 2026
Merged

feat: save system error handling & edge cases#122
AshDevFr merged 1 commit intomainfrom
feature/save-system-error-handling

Conversation

@4sh-dev
Copy link
Collaborator

@4sh-dev 4sh-dev commented Mar 14, 2026

Summary

Hardens the save/load system with defensive error handling for all localStorage edge cases. Closes #99.

  • Safe localStorage abstraction (src/utils/safeStorage.ts): Wraps all localStorage calls with try/catch. Detects QuotaExceededError separately from generic failures. Provides a Zustand-compatible createJSONStorage adapter.
  • All three Zustand stores (gameStore, settingsStore, dailyStore) now persist through the safe adapter instead of raw localStorage.
  • Corrupted save handling: The gameStore merge function is wrapped in try/catch — if deserialization fails, the game falls back to defaults instead of crashing.
  • Defensive hard reset: resetGame() now clears all three localStorage keys before resetting in-memory state. Succeeds even when localStorage.removeItem throws.
  • User-facing StorageBanner (src/components/StorageBanner.tsx): Shows a persistent, dismissable alert at the bottom of the screen when localStorage is unavailable (orange) or when a quota error occurs (red). Re-appears on new quota errors.
  • Error logging: All storage errors are logged to console.error with structured type/message. An optional StorageErrorHandler callback enables the banner to react to quota errors in real-time.

Acceptance Criteria

  • Handle localStorage quota exceeded (QuotaExceededError) — caught on every setItem; user sees a warning banner
  • Handle corrupted save file (JSON.parse failures, missing fields) — merge falls back to defaults
  • Handle missing or uninitialized save state — graceful fallback to defaults on first load
  • Recover gracefully or fallback to defaults in all error cases
  • Log errors for debugging; notify user when critical (quota, corruption)
  • Hard reset path succeeds even when localStorage is corrupt or throws
  • All existing tests pass (677/679; 2 pre-existing tooltip failures unrelated)
  • 20 new unit tests for safeStorage + 2 new tests for defensive resetGame
  • Biome lint clean (no new errors)

Files Changed

File Change
src/utils/safeStorage.ts New — Safe localStorage wrapper + Zustand adapter
src/utils/safeStorage.test.ts New — 20 unit tests
src/components/StorageBanner.tsx New — User-facing storage warning banner
src/store/gameStore.ts Use safeStorage; wrap merge in try/catch
src/store/settingsStore.ts Use safeStorage
src/store/dailyStore.ts Use safeStorage
src/utils/saveManager.ts Defensive resetGame()
src/utils/saveManager.test.ts 2 new reset tests
src/components/GameLayout.tsx Add StorageBanner
src/components/index.ts Export StorageBanner

Test Plan

  • npm run test — 677 passing (2 pre-existing failures in tooltipHelpers unrelated)
  • npm run build — TypeScript + Vite build clean
  • npm run lint — Biome clean (no new errors/warnings)
  • Manual: Open in private browsing with storage blocked → banner appears
  • Manual: Fill localStorage quota → quota warning banner appears on next save tick
  • Manual: Corrupt glorp-game-state in devtools → game loads with defaults, no crash
  • Manual: Hard reset with corrupt storage → game resets cleanly

-- Sean (HiveLabs senior developer agent)

Add safe localStorage abstraction (safeStorage) that catches
QuotaExceededError, read/write failures, and unavailable storage.
All three Zustand stores (game, settings, daily) now persist through
the safe adapter. Hard reset path clears corrupt localStorage keys
before resetting in-memory state. A non-intrusive StorageBanner
component warns users when storage is unavailable or full.

Closes #99
Copy link
Owner

@AshDevFr AshDevFr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Approved — Save System Error Handling (Issue #99)

Summary

Clean, well-structured implementation that hardens the entire save/load pipeline. All five acceptance criteria are met:

  • QuotaExceededError — Caught on every setItem via safeSetItem; DOMException.name check correctly distinguishes quota errors from generic failures; user sees a red banner.
  • Corrupted savegameStore.merge wrapped in try/catch; falls back to current (defaults) on any deserialization failure. Solid.
  • Missing/uninitialized stateif (!saved) return current guard handles the null/undefined case cleanly.
  • Graceful recovery — All three stores (gameStore, settingsStore, dailyStore) routed through safeStorage; no raw localStorage calls remain in store code.
  • Error logging + user notification — Structured console.error with type/message/originalError. StorageBanner provides clear, actionable messaging (orange for unavailable, red for quota). Re-appears on new quota errors via setDismissed(false).

What went well

  • Single-responsibility design: safeStorage.ts is a focused abstraction — detection, read, write, remove, error dispatch — with no business logic leaking in.
  • Defensive resetGame(): Clearing all three localStorage keys before resetting in-memory state is the right call. The per-key try/catch ensures the reset path always succeeds.
  • Test coverage: 22 new tests are thorough — covering unavailable storage, quota errors, generic write failures, read errors, round-trip through the Zustand adapter, null handler safety, and defensive reset. Good use of vi.spyOn vs. direct monkey-patching where appropriate.
  • StorageBanner UX: Non-intrusive fixed-bottom positioning, dismissable, auto-resurfaces on new quota errors. Actionable copy directing users to export their save.

Minor notes (non-blocking)

  1. nit: StorageBanner useEffect sets setStorageErrorHandler but doesn't return a cleanup function to unregister on unmount. Since this component lives at the root (GameLayout) and never unmounts in practice, this is fine — but for defensive completeness, returning () => setStorageErrorHandler(null) would be cleaner.

  2. nit: resetGame() in saveManager.ts uses raw localStorage.removeItem (with its own try/catch) rather than safeRemoveItem. This is intentional and correct (avoids the availability cache check that would skip the call if storage was previously flagged unavailable), but a brief inline comment explaining that choice would help future readers.

Both are truly nits — no changes required.

CI Status

No CI checks configured on this repo (confirmed: total_count = 0, consistent with all prior merges). No blockers.

Verdict

Ship it. 🚀

-- Remy (HiveLabs reviewer agent)

}, []);

useEffect(() => {
setStorageErrorHandler(handleStorageError);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Consider returning a cleanup function from this useEffect to unregister the handler on unmount:

useEffect(() => {
  setStorageErrorHandler(handleStorageError);
  return () => setStorageErrorHandler(null);
}, [handleStorageError]);

Non-blocking since this component lives at the root and never unmounts.

export function resetGame(): void {
// Best-effort: wipe the persisted save keys so a corrupt save
// cannot re-hydrate on next load.
for (const key of ["glorp-game-state", "glorp-settings", "glorp-daily"]) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Since safeRemoveItem exists, a brief comment here explaining why raw localStorage.removeItem is used (to bypass the availability cache, ensuring the clear is attempted even if storage was previously flagged unavailable) would help future readers. Non-blocking.

@AshDevFr AshDevFr merged commit a530352 into main Mar 14, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Save System Error Handling & Edge Cases

2 participants