refactor: complete error handling standardization (issue #101)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m26s
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:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user