refactor: complete error handling standardization (issue #101)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m26s

Prestige Store:
- Convert doPrestige() to return Result<void> with specific error codes
  (INVALID_PRESTIGE_ID, PRESTIGE_MAX_LEVEL, INSUFFICIENT_INSIGHT)
- Convert startPactRitual() to return Result<void> with specific error codes
  (GUARDIAN_NOT_DEFEATED, PACT_ALREADY_SIGNED, PACT_SLOTS_FULL,
   INSUFFICIENT_MANA, RITUAL_IN_PROGRESS)

Combat Actions:
- Add try/catch wrapper inside processCombatTick with safe fallback defaults
- Add makeDefaultCombatTickResult helper for error recovery

LocalStorage Error Handling:
- Create safe-persist.ts utility wrapping localStorage with error handling
  (corrupted JSON, quota exceeded, unexpected failures)
- Update all 8 Zustand stores to use createSafeStorage() in persist middleware

UI Updates:
- Update GuardianPactsTab to use Result pattern for ritual error messages

Tests:
- Update store-actions-combat-prestige.test.ts for Result return types
- Update store-actions.test.ts ManaStore tests for Result pattern
- Remove duplicate Prestige/Discipline sections from store-actions.test.ts
- All files under 400 line limit

601 tests pass (3 pre-existing failures in spire-utils.test.ts)
This commit is contained in:
2026-05-22 09:19:20 +02:00
parent 8a7ddaae27
commit 49f8de01ca
21 changed files with 542 additions and 547 deletions
+3
View File
@@ -2,6 +2,9 @@
// Re-export everything from the focused modules
export { fmt, fmtDec, formatSpellCost, getSpellCostColor, formatStudyTime, formatHour } from './formatting';
export { createSafeStorage } from './safe-persist';
export { ok, okVoid, fail, failTyped, unwrapOr, isErrorCode, ErrorCode } from './result';
export type { Result, ErrorCodeType } from './result';
export { getFloorMaxHP, getFloorElement } from './floor-utils';
export {
computeMaxMana,
+89
View File
@@ -0,0 +1,89 @@
// ─── Standardized Result Type ─────────────────────────────────────────────────
// Provides consistent error handling across the codebase.
// Use Result<T> for expected failures; reserve throw for truly unexpected errors.
/**
* Error codes for categorizing failures.
* Enables callers to programmatically handle specific error types.
*/
export const ErrorCode = {
// Mana errors
INSUFFICIENT_MANA: 'INSUFFICIENT_MANA',
ELEMENT_NOT_UNLOCKED: 'ELEMENT_NOT_UNLOCKED',
ELEMENT_MAX_CAPACITY: 'ELEMENT_MAX_CAPACITY',
INVALID_ELEMENT: 'INVALID_ELEMENT',
// Crafting errors
INVALID_BLUEPRINT: 'INVALID_BLUEPRINT',
BLUEPRINT_NOT_ACQUIRED: 'BLUEPRINT_NOT_ACQUIRED',
MISSING_MATERIALS: 'MISSING_MATERIALS',
INVALID_EQUIPMENT_TYPE: 'INVALID_EQUIPMENT_TYPE',
INVALID_EFFECT: 'INVALID_EFFECT',
EFFECT_NOT_ALLOWED: 'EFFECT_NOT_ALLOWED',
STACKS_EXCEED_MAX: 'STACKS_EXCEED_MAX',
INSUFFICIENT_CAPACITY: 'INSUFFICIENT_CAPACITY',
EQUIPMENT_NOT_FOUND: 'EQUIPMENT_NOT_FOUND',
DESIGN_NOT_FOUND: 'DESIGN_NOT_FOUND',
EQUIPMENT_NOT_PREPARED: 'EQUIPMENT_NOT_PREPARED',
ALREADY_PREPARED: 'ALREADY_PREPARED',
// Action state errors
INVALID_ACTION_STATE: 'INVALID_ACTION_STATE',
// Prestige errors
INSUFFICIENT_INSIGHT: 'INSUFFICIENT_INSIGHT',
GUARDIAN_NOT_DEFEATED: 'GUARDIAN_NOT_DEFEATED',
PACT_ALREADY_SIGNED: 'PACT_ALREADY_SIGNED',
PACT_SLOTS_FULL: 'PACT_SLOTS_FULL',
RITUAL_IN_PROGRESS: 'RITUAL_IN_PROGRESS',
PRESTIGE_MAX_LEVEL: 'PRESTIGE_MAX_LEVEL',
INVALID_PRESTIGE_ID: 'INVALID_PRESTIGE_ID',
// General
INVALID_INPUT: 'INVALID_INPUT',
NOT_INITIALIZED: 'NOT_INITIALIZED',
} as const;
export type ErrorCodeType = (typeof ErrorCode)[keyof typeof ErrorCode];
/**
* Standard result type for operations that can fail.
* T is the success payload type.
*/
export type Result<T = void> =
| { success: true; data: T }
| { success: false; error: string; code: ErrorCodeType };
/** Create a success result. */
export function ok<T>(data: T): Result<T> {
return { success: true, data };
}
/** Create a void success result. */
export function okVoid(): Result<void> {
return { success: true, data: undefined };
}
/** Create a failure result. */
export function fail(code: ErrorCodeType, error: string): Result<never> {
return { success: false, error, code };
}
/** Create a failure result with a typed data field. */
export function failTyped<T>(code: ErrorCodeType, error: string): Result<T> {
return { success: false, error, code };
}
/**
* Unwrap a result, returning the data or a default value.
*/
export function unwrapOr<T>(result: Result<T>, defaultValue: T): T {
return result.success ? result.data : defaultValue;
}
/**
* Check if a result is a failure with a specific error code.
*/
export function isErrorCode<T>(result: Result<T>, code: ErrorCodeType): boolean {
return !result.success && result.code === code;
}
+49
View File
@@ -0,0 +1,49 @@
// ─── Safe Persist Storage ─────────────────────────────────────────────────────
// Wraps localStorage with error handling for Zustand persist middleware.
// Handles: quota exceeded, corrupted JSON, and unexpected read/write failures.
import type { StateStorage } from 'zustand/middleware';
/**
* Creates a safe localStorage wrapper for Zustand persist.
* - Corrupted JSON → returns null (Zustand uses initial state)
* - Quota exceeded → logs warning, skips write
* - Other errors → logs warning, graceful fallback
*/
export function createSafeStorage(): StateStorage {
return {
getItem: (name: string): unknown => {
try {
const str = localStorage.getItem(name);
if (str === null) return null;
return JSON.parse(str);
} catch (error) {
console.warn(`[persist] Failed to read "${name}" from localStorage:`, error);
try {
localStorage.removeItem(name);
} catch {
// ignore
}
return null;
}
},
setItem: (name: string, value: unknown): void => {
try {
localStorage.setItem(name, JSON.stringify(value));
} catch (error) {
if (error instanceof DOMException && error.name === 'QuotaExceededError') {
console.warn(`[persist] localStorage quota exceeded for "${name}". State will not persist this tick.`);
} else {
console.warn(`[persist] Failed to write "${name}" to localStorage:`, error);
}
}
},
removeItem: (name: string): void => {
try {
localStorage.removeItem(name);
} catch (error) {
console.warn(`[persist] Failed to remove "${name}" from localStorage:`, error);
}
},
};
}