Skip to content

Wire BibleReader highlights to the highlights API (YPE-1034) (1/4)#283

Merged
cameronapak merged 8 commits into
mainfrom
claude/ype-1034-pr1-highlights-api
Jul 21, 2026
Merged

Wire BibleReader highlights to the highlights API (YPE-1034) (1/4)#283
cameronapak merged 8 commits into
mainfrom
claude/ype-1034-pr1-highlights-api

Conversation

@cameronapak

@cameronapak cameronapak commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

BibleReader renders a signed-in user's server highlights, dark-launched behind the HIGHLIGHTS_LIVE flag (off by default). Restored from consolidated #288 and rebased onto current main.

Stack: 1/4 (this PR) → 2/4 #285 → 3/4 #286 → 4/4 #294 · review the stack bottom-up

Changes

  1. BibleReader shows the user's highlights from the highlights API when HIGHLIGHTS_LIVE is enabled — no visible change while the flag is off.
  2. Highlight data refetches when the flag flips on, clears when it flips off, and latest-wins on overlapping responses (useApiData hardening in hooks).
  3. USFM range utilities map highlight verse ranges for rendering.
  4. ADR + glossary document the server-only highlights decision (no local persistence).

Test plan

  • pnpm build && pnpm typecheck && pnpm lint && pnpm test at this branch: all green (core 315 / hooks 281 / ui 211).
  • Storybook integration tests run in CI (not runnable in the local sandbox).

Greptile Summary

This PR wires BibleReader's highlight system to the server-side highlights API behind an internal HIGHLIGHTS_LIVE dark-launch flag (currently false), replacing the temporary localStorage store with a server-only model. It also hardens useApiData with a monotonic sequence number to suppress stale responses from both effect-initiated and refetch-initiated requests, and exports YouVersionAuthContext for consumers that must tolerate a missing auth provider.

  • useApiData hardening: replaces per-effect canceled closures with a shared requestSeqRef; adds enabled to effect deps so a false\u2192true flip fires the fetch; clears data/error on disable to prevent stale account data from lingering across sessions.
  • useBibleReaderHighlights: new seam hook with optimistic overlay, scope-gated confirmed-verse drain, auth-tolerant context read, and dark-launch gate. All previous-review bugs are fixed and regression-tested.
  • usfm-ranges.ts: pure utility for collapsing verse lists into USFM range strings (JHN.3.16-18), fully unit-tested.

Confidence Score: 5/5

Safe to merge — the entire highlight path is dark-launched behind HIGHLIGHTS_LIVE=false, so no changed behavior reaches production until the flag flips.

The two observations are both narrow and flag-gated: the stale captured-closure refetch interaction only matters while HIGHLIGHTS_LIVE is on, and the module-level flag state is only mutable via an @internal test helper. The core logic is correct and thoroughly tested.

useApiData.ts and use-bible-reader-highlights.ts warrant a second look before HIGHLIGHTS_LIVE is flipped: the stale-closure refetch race should be resolved in PR 2 as planned.

Important Files Changed

Filename Overview
packages/hooks/src/useApiData.ts Replaced per-effect canceled closure with a monotonic requestSeqRef sequence number; adds enabled to effect deps; clears data/error on disable. Subtle interaction with stale captured closures from createHighlight flagged as a known deferred concurrency issue.
packages/ui/src/components/use-bible-reader-highlights.ts New hook: optimistic overlay with snapshot/revert, scope-gated confirmed-verse drain, auth-tolerant context read, dark-launch gate. All previous-review findings addressed.
packages/ui/src/lib/feature-flags.ts New dark-launch module for HIGHLIGHTS_LIVE=false. Module-level mutable state is safe today (only used by 'use client' modules) but would be problematic if imported server-side.
packages/ui/src/components/bible-reader.tsx Removes localStorage highlight store, delegates to useBibleReaderHighlights. Mechanical and clean.
packages/hooks/src/context/index.ts Exports YouVersionAuthContext via context/index.ts per the established pattern.
packages/ui/src/lib/usfm-ranges.ts New utility collapsing verse lists into contiguous USFM range strings. Well-tested.
packages/hooks/src/useApiData.test.tsx New tests covering enabled false→true transitions and stale response suppression with deferred promises.
packages/ui/src/components/use-bible-reader-highlights.test.tsx Comprehensive unit tests including regression coverage for all three previously-reviewed bugs.
packages/ui/src/components/use-bible-reader-highlights.integration.test.tsx Integration tests against the real useHighlights/useApiData stack covering auth-resolves-after-mount and sign-out.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant BR as BibleReader
    participant UBH as useBibleReaderHighlights
    participant UH as useHighlights
    participant UAD as useApiData
    participant API as Highlights API
    Note over BR,API: Flag ON + signed-in
    BR->>UBH: "mount {versionId, book, chapter}"
    UBH->>UH: useHighlights passage_id JHN.3 enabled true
    UH->>UAD: fetchFn getHighlights
    UAD->>API: GET highlights JHN.3
    API-->>UAD: verse highlights data
    UAD-->>UBH: highlightedVerses
    UBH-->>BR: highlightedVerses map
    Note over BR,API: User taps color
    BR->>UBH: apply color verses
    UBH->>UBH: patchOverlay optimistic
    UBH->>UH: createHighlight range USFM
    UH->>API: POST highlights
    API-->>UH: 201 Created
    UH->>UAD: refetch
    UAD->>API: GET highlights JHN.3
    API-->>UAD: updated data
    UBH->>UBH: drain confirmed overlay entries
    Note over BR,API: Sign-out
    UBH->>UH: enabled false
    UAD->>UAD: setData null
    UBH-->>BR: highlightedVerses empty
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant BR as BibleReader
    participant UBH as useBibleReaderHighlights
    participant UH as useHighlights
    participant UAD as useApiData
    participant API as Highlights API
    Note over BR,API: Flag ON + signed-in
    BR->>UBH: "mount {versionId, book, chapter}"
    UBH->>UH: useHighlights passage_id JHN.3 enabled true
    UH->>UAD: fetchFn getHighlights
    UAD->>API: GET highlights JHN.3
    API-->>UAD: verse highlights data
    UAD-->>UBH: highlightedVerses
    UBH-->>BR: highlightedVerses map
    Note over BR,API: User taps color
    BR->>UBH: apply color verses
    UBH->>UBH: patchOverlay optimistic
    UBH->>UH: createHighlight range USFM
    UH->>API: POST highlights
    API-->>UH: 201 Created
    UH->>UAD: refetch
    UAD->>API: GET highlights JHN.3
    API-->>UAD: updated data
    UBH->>UBH: drain confirmed overlay entries
    Note over BR,API: Sign-out
    UBH->>UH: enabled false
    UAD->>UAD: setData null
    UBH-->>BR: highlightedVerses empty
Loading

Reviews (4): Last reviewed commit: "refactor: apply non-blocking review sugg..." | Re-trigger Greptile

@changeset-bot

changeset-bot Bot commented Jul 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a36263a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@youversion/platform-react-hooks Patch
@youversion/platform-react-ui Patch
vite-react Patch
@youversion/platform-core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

cameronapak and others added 4 commits July 20, 2026 14:43
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dark-launch flag

Replace the localStorage highlight store (YPE-642 stand-in) with real API
wiring per YPE-1034 ADR-001: highlights are server-only account data, deleted
locally with no migration.

- New internal useBibleReaderHighlights seam: fetches the current chapter's
  highlights via useHighlights (chapter USFM passage_id, keyed on version),
  renders through an in-memory optimistic overlay, reverts + console.error on
  write failure (toasts and 401/403 handling are PR 2)
- Contiguous verse runs collapse to range USFMs on the wire (usfm-ranges.ts,
  mirroring the verse-share run-grouping idiom); colors sent as lowercase hex
- Internal HIGHLIGHTS_LIVE dark-launch flag (off, not exported from the
  package entry) gates fetches, writes, and rendering; setHighlightsLive() is
  the test/Storybook-only override
- Auth-gated: reads YouVersionAuthContext directly (now exported from hooks)
  so a missing auth provider degrades to signed-out instead of the useYVAuth
  throw; sign-out un-renders highlights immediately
- better-result added to the ui package for error typing at the new seam's
  write boundary; core's throwing clients are untouched

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… responses

Adversarial review of the highlights wiring surfaced one blocker and two
races, all rooted in useApiData:

- The fetch effect ignored enabled transitions: a hook that mounted disabled
  (auth still resolving) never fetched once enabled flipped true, so a
  signed-in reader rendered zero highlights until a write or navigation.
  enabled now rides alongside the caller-supplied deps.
- Disabling kept the last response, so stale account data could render across
  sign-out or a host-controlled auth user switch. Disabling now clears data
  and error.
- refetch-initiated requests escaped cancellation: a stale refetch for a
  previous chapter resolving late could clobber the new chapter's data. All
  requests now go through a monotonic sequence; only the latest-issued
  request may commit state.

In the BibleReader seam hook:

- Successfully settled writes now hand their verses to a confirmed set whose
  overlay entries are dropped when the post-write refetch lands, so server
  truth wins again instead of the optimistic entry masking later remote
  changes until navigation.
- Documented the two remaining apply/remove concurrency windows (in-flight
  POST vs DELETE ordering, snapshot revert vs concurrent write) at the write
  boundary; a real operation queue is deliberately PR 2.

New tests: useApiData enabled transitions and stale-response handling, and a
seam-hook integration test through the real useHighlights/useApiData path
(module-level mocks had hidden the enabled-flip bug).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cameronapak cameronapak reopened this Jul 20, 2026
@cameronapak
cameronapak force-pushed the claude/ype-1034-pr1-highlights-api branch from 5b7d334 to 7f66a99 Compare July 20, 2026 20:15
@cameronapak cameronapak changed the title YPE-1034: wire BibleReader highlights to the highlights API (1/3, dark-launched) Wire BibleReader highlights to the highlights API (YPE-1034) (1/4) Jul 20, 2026
@cameronapak
cameronapak marked this pull request as ready for review July 20, 2026 20:18
Comment thread packages/ui/src/components/use-bible-reader-highlights.ts Outdated
Comment thread packages/ui/src/components/use-bible-reader-highlights.ts Outdated
Comment thread packages/ui/src/components/use-bible-reader-highlights.ts Outdated
…lay assertions

Cross-scope contamination: confirmedVersesRef enrolled a settled write's
verses regardless of whether the user had since navigated to another
chapter/version, letting the drain effect erase the new scope's optimistic
overlay entry (verse numbers collide across scopes). Capture the overlay
scope at write start and only enroll on settle if it still matches the
current scope ref; also clear the confirmed set on scope change alongside
the existing render-time overlay reset.

Also replace two non-null assertions (`overlay[verse]!`, `snapshot[verse]!`)
with `as string | null` casts: null means "optimistically removed" in
HighlightOverlay, so `!` stripped a legitimate value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/ui/src/components/use-bible-reader-highlights.ts
The success path already skipped enrolling verses in confirmedVersesRef when
the write's captured scope no longer matched. The failure path had the same
gap: after navigating chapters, a stale write's revertOverlay ran against a
snapshot that predates the reset, so its absent entries would `delete` the new
chapter's optimistic verses (verse numbers collide across scopes).

Gate both revertOverlay calls on `overlayScopeRef.current === scopeAtWrite`,
matching the success path. console.error logging stays unconditional. Update
the invariant comment to cover both settle paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@jaredhightower-youversion jaredhightower-youversion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a review using /ponytail-review skill and it found a few things:

@jaredhightower-youversion jaredhightower-youversion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a simplification-focused pass over this PR — not looking for bugs (the earlier greptile threads covered those and they're all resolved; I haven't re-raised any of them), just places where the diff could be smaller and easier to maintain. Found five. None of them are blockers, but together they'd cut roughly 70 lines and one dependency, and each comment below explains what's going on and includes the replacement code so it should be quick to action.

@jaredhightower-youversion jaredhightower-youversion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added conventional comments format, all non-blocking.

Comment thread packages/ui/package.json
Comment thread packages/ui/src/components/use-bible-reader-highlights.ts
Comment thread packages/hooks/src/useApiData.test.tsx Outdated
Comment thread packages/ui/src/lib/feature-flags.ts
Comment thread CONTEXT.md
Comment thread packages/hooks/src/useApiData.ts
Comment thread packages/ui/src/components/use-bible-reader-highlights.ts

@Dustin-Kelley Dustin-Kelley left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty happy with this! I think a lot of lines will be removed when/if we add a proper query library in the future. Just address Jareds comment's and I can approve.

- use Promise.withResolvers in useApiData tests
- collapse feature-flags.ts to minimal shape
- trim CONTEXT.md glossary to implemented terms

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cameronapak
cameronapak merged commit c85d8de into main Jul 21, 2026
8 checks passed
@cameronapak
cameronapak deleted the claude/ype-1034-pr1-highlights-api branch July 21, 2026 15:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants