Skip to content

Extract shared mic-capture re-entrancy guard from useSingToScore/useSingToVerify #4283

Description

@atomantic

Background

`useSingToScore.js` and `useSingToVerify.js` (both in `client/src/hooks/`) each independently implement the same re-entrancy-guard pattern for a pending `getUserMedia` permission prompt: a `startPendingRef` boolean plus a `requestGenerationRef` incrementing counter, with every async continuation (the `getUserMedia` catch, the post-await mount/generation check, the metronome-start catch) gated on `requestGeneration === requestGenerationRef.current`, and a `cancel()` callback that bumps the generation, tears down, and resets phase — invoked both directly and via a ref-mirrored unmount effect.

`useSingToScore`'s copy (added in #4236, "prevent pending sing-to-score mic leaks") was deliberately written to mirror `useSingToVerify.js` lines ~78-120 — the issue that shipped it explicitly specified duplicating the sibling's pattern rather than extracting it, to keep that PR scoped as a narrow bug fix.

That leaves ~40 lines of subtle, correctness-critical concurrency logic living in two places with nothing enforcing they stay in sync — a real drift risk if either hook's edge cases evolve independently.

Decision

Extract a shared hook, e.g. `useAsyncCaptureGuard` in `client/src/hooks/`, exposing something like `{ tryStart(), cancel(), isCurrent(gen) }` over the two refs, parameterized by the caller's `teardown`/idle-reset callback. Have both `useSingToScore.js` and `useSingToVerify.js` consume it instead of each rolling their own copy.

Note other hooks (`useShellSession`, `useMidiNotes`) also roll similar-but-distinct generation-guard logic inline — check during implementation whether either is close enough to the same shape to consume the new hook too, but don't force a fit if their semantics diverge meaningfully.

Acceptance criteria

  • New hook lives in `client/src/hooks/`, re-exported from the barrel `index.js`, with a one-line row in `client/src/hooks/README.md` (per the module-organization maintenance rule in the root CLAUDE.md).
  • `useSingToScore.js` and `useSingToVerify.js` both consume it; their existing test suites (`useSingToScore.test.js`, `useSingToVerify.test.js`) continue to pass unchanged in behavior — this is a refactor, not a behavior change.
  • No new re-entrancy edge cases introduced; the "allows only one microphone request while permission is pending" / "stops a permission-pending stream when capture is cancelled" style tests should pass identically for both hooks.

Found during the `/simplify` pass on #4236 (PR shipping the `useSingToScore` mic-leak fix) — deliberately left out of that PR, which was scoped to mirroring the sibling pattern, not extracting it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

code-qualityProposed from a module-hygiene auditplanTracked by /do:replan

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions