Files
initiative/apps/web/src/persistence/encounter-storage.ts
Lukas 1de00e3d8e
All checks were successful
CI / check (push) Successful in 1m16s
CI / build-image (push) Has been skipped
Move entity rehydration to domain layer, fix tempHp gap
Rehydration functions (reconstructing typed domain objects from untyped
JSON) lived in persistence adapters, duplicating domain validation.
Adding a field required updating both the domain type and a separate
adapter function — the adapter was missed for `level`, silently dropping
it on reload. Now adding a field only requires updating the domain type
and its co-located rehydration function.

- Add `rehydratePlayerCharacter` and `rehydrateCombatant` to domain
- Persistence adapters delegate to domain instead of reimplementing
- Add `tempHp` validation (was silently dropped during rehydration)
- Tighten initiative validation to integer-only
- Exhaustive domain tests (53 cases); adapter tests slimmed to round-trip
- Remove stale `jsinspect-plus` Knip ignoreDependencies entry

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 11:12:41 +01:00

68 lines
1.6 KiB
TypeScript

import {
type Combatant,
createEncounter,
type Encounter,
isDomainError,
rehydrateCombatant,
} from "@initiative/domain";
const STORAGE_KEY = "initiative:encounter";
export function saveEncounter(encounter: Encounter): void {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(encounter));
} catch {
// Silently swallow errors (quota exceeded, storage unavailable)
}
}
export function rehydrateEncounter(parsed: unknown): Encounter | null {
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
return null;
const obj = parsed as Record<string, unknown>;
if (!Array.isArray(obj.combatants)) return null;
if (typeof obj.activeIndex !== "number") return null;
if (typeof obj.roundNumber !== "number") return null;
const combatants = obj.combatants as unknown[];
// Handle empty encounter (cleared state) directly — createEncounter rejects empty arrays
if (combatants.length === 0) {
return {
combatants: [],
activeIndex: 0,
roundNumber: 1,
};
}
const rehydrated: Combatant[] = [];
for (const c of combatants) {
const result = rehydrateCombatant(c);
if (result === null) return null;
rehydrated.push(result);
}
const encounter = createEncounter(
rehydrated,
obj.activeIndex,
obj.roundNumber,
);
if (isDomainError(encounter)) return null;
return encounter;
}
export function loadEncounter(): Encounter | null {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw === null) return null;
const parsed: unknown = JSON.parse(raw);
return rehydrateEncounter(parsed);
} catch {
return null;
}
}