146 lines
7.5 KiB
Markdown
146 lines
7.5 KiB
Markdown
# Tasks: Persist Encounter
|
|
|
|
**Input**: Design documents from `/specs/008-persist-encounter/`
|
|
**Prerequisites**: plan.md, spec.md, research.md, data-model.md
|
|
|
|
**Tests**: Included -- persistence logic warrants unit tests to cover serialization, validation, and error handling.
|
|
|
|
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
|
|
|
|
## Format: `[ID] [P?] [Story] Description`
|
|
|
|
- **[P]**: Can run in parallel (different files, no dependencies)
|
|
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
|
|
- Include exact file paths in descriptions
|
|
|
|
## Phase 1: Setup
|
|
|
|
**Purpose**: Create the persistence module structure
|
|
|
|
- [x] T001 Create persistence module directory at `apps/web/src/persistence/`
|
|
|
|
---
|
|
|
|
## Phase 2: Foundational (Blocking Prerequisites)
|
|
|
|
**Purpose**: Core storage adapter that all user stories depend on
|
|
|
|
- [x] T002 Implement `saveEncounter(encounter: Encounter): void` function in `apps/web/src/persistence/encounter-storage.ts` that serializes encounter state to localStorage under key `"initiative:encounter"` using `JSON.stringify`, wrapped in try/catch that silently swallows errors (quota exceeded, storage unavailable)
|
|
- [x] T003 Implement `loadEncounter(): Encounter | null` function in `apps/web/src/persistence/encounter-storage.ts` that reads from localStorage key `"initiative:encounter"`, parses JSON, performs structural shape checks (object with `combatants` array, `activeIndex` number, `roundNumber` number; each combatant has `id` string and `name` string), rehydrates `CombatantId` values via `combatantId()`, validates through `createEncounter`, and returns `null` on any failure (parse error, shape mismatch, domain validation failure)
|
|
- [x] T004 Write unit tests for `saveEncounter` and `loadEncounter` in `apps/web/src/persistence/__tests__/encounter-storage.test.ts` covering: round-trip save/load preserves encounter state, `loadEncounter` returns `null` when localStorage is empty, returns `null` for non-JSON strings, returns `null` for JSON missing required fields, returns `null` for invalid encounter data (e.g. empty combatants array, out-of-bounds activeIndex)
|
|
|
|
**Checkpoint**: Storage adapter is complete and tested -- user story implementation can now begin
|
|
|
|
---
|
|
|
|
## Phase 3: User Story 1 - Encounter Survives Page Reload (Priority: P1) MVP
|
|
|
|
**Goal**: Encounter state persists across page reloads with zero data loss
|
|
|
|
**Independent Test**: Set up an encounter with combatants, initiative values, and advanced turns. Call `saveEncounter`, then `loadEncounter` and verify all state matches. In the hook, verify `useEffect` triggers `saveEncounter` on state changes.
|
|
|
|
### Tests for User Story 1
|
|
|
|
- [x] T005 [US1] Write tests in `apps/web/src/persistence/__tests__/encounter-storage.test.ts` for: round-trip preserves combatant IDs, names, and initiative values; round-trip preserves activeIndex and roundNumber; saving after modifications (add/remove combatant, change initiative) persists the latest state
|
|
|
|
### Implementation for User Story 1
|
|
|
|
- [x] T006 [US1] Modify `useEncounter` hook in `apps/web/src/hooks/use-encounter.ts` to initialize state from `loadEncounter()` -- if it returns a valid encounter, use it instead of `createDemoEncounter()`; derive `nextId` counter from highest numeric suffix in existing combatant IDs (parse `c-{N}` pattern)
|
|
- [x] T007 [US1] Add a `useEffect` in `apps/web/src/hooks/use-encounter.ts` that calls `saveEncounter(encounter)` whenever the encounter state changes
|
|
|
|
**Checkpoint**: User Story 1 is fully functional -- encounter survives page reload
|
|
|
|
---
|
|
|
|
## Phase 4: User Story 2 - Fresh Start with No Saved Data (Priority: P2)
|
|
|
|
**Goal**: First-time users see the default demo encounter
|
|
|
|
**Independent Test**: With no localStorage data, load the app and verify the demo encounter (Aria, Brak, Cael) is displayed.
|
|
|
|
### Implementation for User Story 2
|
|
|
|
- [x] T008 [US2] Verify and document in tests at `apps/web/src/persistence/__tests__/encounter-storage.test.ts` that `loadEncounter()` returns `null` when localStorage has no `"initiative:encounter"` key, confirming the `useEncounter` hook falls back to `createDemoEncounter()`
|
|
|
|
**Checkpoint**: User Story 2 confirmed -- no saved data results in demo encounter
|
|
|
|
---
|
|
|
|
## Phase 5: User Story 3 - Graceful Handling of Corrupt Data (Priority: P3)
|
|
|
|
**Goal**: Corrupt or invalid saved data never crashes the app; falls back to demo encounter
|
|
|
|
**Independent Test**: Write various malformed values to the `"initiative:encounter"` localStorage key and verify `loadEncounter()` returns `null` for each.
|
|
|
|
### Tests for User Story 3
|
|
|
|
- [x] T009 [US3] Add tests in `apps/web/src/persistence/__tests__/encounter-storage.test.ts` for corrupt data scenarios: non-object JSON (string, number, array, null), object with wrong types for fields (combatants as string, activeIndex as string), combatant entries missing `id` or `name`, valid JSON structure but domain-invalid data (zero combatants, negative roundNumber)
|
|
|
|
### Implementation for User Story 3
|
|
|
|
- [x] T010 [US3] Verify `loadEncounter()` structural checks in `apps/web/src/persistence/encounter-storage.ts` cover all corrupt data scenarios from T009 -- adjust shape validation if any test cases reveal gaps
|
|
|
|
**Checkpoint**: All corrupt data scenarios handled gracefully
|
|
|
|
---
|
|
|
|
## Phase 6: Polish & Cross-Cutting Concerns
|
|
|
|
- [x] T011 Run `pnpm check` to verify formatting, linting, type checking, and all tests pass
|
|
- [x] T012 Run quickstart.md manual verification steps to validate end-to-end behavior
|
|
|
|
---
|
|
|
|
## Dependencies & Execution Order
|
|
|
|
### Phase Dependencies
|
|
|
|
- **Setup (Phase 1)**: No dependencies
|
|
- **Foundational (Phase 2)**: Depends on Phase 1 -- BLOCKS all user stories
|
|
- **User Story 1 (Phase 3)**: Depends on Phase 2
|
|
- **User Story 2 (Phase 4)**: Depends on Phase 2 (independent of US1)
|
|
- **User Story 3 (Phase 5)**: Depends on Phase 2 (independent of US1, US2)
|
|
- **Polish (Phase 6)**: Depends on all user stories complete
|
|
|
|
### User Story Dependencies
|
|
|
|
- **User Story 1 (P1)**: Depends on Foundational only. Core save/load wiring in the hook.
|
|
- **User Story 2 (P2)**: Depends on Foundational only. Verifies the null-fallback path.
|
|
- **User Story 3 (P3)**: Depends on Foundational only. Hardens validation against corrupt data.
|
|
|
|
### Parallel Opportunities
|
|
|
|
- T002 and T003 are sequential (same file, T003 depends on T002's storage key constant)
|
|
- T005 and T006 can run in parallel (test file vs hook file)
|
|
- T008 and T009 target the same test file as T004/T005 (`encounter-storage.test.ts`); sequence them after T005 to avoid merge conflicts. T006/T007 (hook file) can still run in parallel with test tasks.
|
|
- US2 and US3 can proceed in parallel after Foundational phase
|
|
|
|
---
|
|
|
|
## Implementation Strategy
|
|
|
|
### MVP First (User Story 1 Only)
|
|
|
|
1. Complete Phase 1: Setup (T001)
|
|
2. Complete Phase 2: Foundational (T002-T004)
|
|
3. Complete Phase 3: User Story 1 (T005-T007)
|
|
4. **STOP and VALIDATE**: Run `pnpm check`, manually test reload behavior
|
|
5. This alone delivers the core value of the feature
|
|
|
|
### Incremental Delivery
|
|
|
|
1. Setup + Foundational -> Storage adapter ready
|
|
2. Add User Story 1 -> Reload persistence works (MVP!)
|
|
3. Add User Story 2 -> First-time experience confirmed
|
|
4. Add User Story 3 -> Corrupt data resilience hardened
|
|
5. Polish -> Final validation
|
|
|
|
---
|
|
|
|
## Notes
|
|
|
|
- All new code is in the adapter layer (`apps/web/`); domain and application packages are unchanged
|
|
- Tests use a localStorage mock (Vitest's jsdom environment or manual mock)
|
|
- The `nextId` counter derivation (T006) is critical to avoid ID collisions after reload
|
|
- Commit after each phase or logical group of tasks
|