Skip to content

useVerseAudio as a testable state machine (net-new) #97

Description

@mattrace-gloo

Current state on main

  • ViewChapter.tsx keeps per-verse UI state in a plain object (verseStates) toggled by a button (:108). No persistence, no engine.
  • No hook coordinates record → stop → persist → play.

Problems / why (anti-patterns to avoid)

PR #82 src/hooks/useVerseAudio.ts (316 lines) is the anti-pattern:

  • Fat hook: imperative IWaveformRef refs, react-native-fs file moves, Alert.alert dialogs, permission prompts, and DB writes (upsertRecording/deleteRecording) all inline in startRecord/stopRecord.
  • State is split across useState + multiple useRefs (recordingStarted, tempRecordingPath, finalRecordingPath) with ad-hoc transitions → untestable, race-prone.
  • Tightly coupled to the Simform library types (PlayerState, RecorderState, FinishMode).

Target design

A small, deterministic state machine driven by typed events, with side effects injected (so it is unit-testable without native modules):

export type VerseAudioState =
 | 'idle' | 'recording' | 'paused' | 'recorded' | 'playing' | 'saving' | 'error';

type Event =
 | { type: 'START' } | { type: 'PAUSE' } | { type: 'RESUME' } | { type: 'STOP' }
 | { type: 'SAVED' } | { type: 'PLAY' } | { type: 'PLAYBACK_END' }
 | { type: 'DELETE' } | { type: 'ERROR'; message: string };

// Pure reducer: (state, event) => nextState — fully unit-testable
export function verseAudioReducer(state: VerseAudioState, e: Event): VerseAudioState;

Implementation steps

  1. Create src/hooks/verseAudioReducer.ts (pure) + tests covering each transition (including ERROR and DELETE).
  2. Create src/hooks/useVerseAudio.ts that composes the reducer + recorder (Recording engine on expo-audio #95) + player (Playback engine on expo-audio + waveform decision #96) + persistence (Multi-take support (take_number / is_latest semantics) #98), with injectable dependencies.
  3. Persist on STOP: move temp file to recordingPath(id) (Audio storage on expo-file-system (net-new) #94) and insert a take (Multi-take support (take_number / is_latest semantics) #98); set state recorded.
  4. Integrate into ViewChapter.tsx, replacing the mock toggle.
  5. No react-native-fs, no Simform types, no DB writes inside reducer.

Acceptance criteria

  • A pure reducer encodes all verse-audio transitions and is fully unit-tested.
  • The hook composes recorder/player/storage/DB via injected deps (no inline native coupling).
  • No react-native-fs and no @simform_solutions/* imports anywhere in the audio path.
  • Per-verse status is derived from the DB, not in-memory refs.
  • ViewChapter.tsx uses the hook instead of the mock toggle.

Test plan / verification

npm run format:check && npm run lint && npm run typecheck && npm test -- --ci

Out of scope

Links

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Dev Ready

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions