Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Release, versioning, or publishing: read `PUBLISHING.md`; decisions live in `doc

## Domain
Domain terms (highlight, passage, Bible version, auth flow): read `CONTEXT.md`.
Bible read cache (opted-in Bible read, remaining lifetime, CachePolicy): read `docs/bible-read-cache.md`.

## Cursor Cloud
Cloud VM, env files, Vite bind, or demo startup: read `docs/cursor-cloud.md`.
Expand Down
52 changes: 52 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,55 @@ survives the full-page redirect round-trip and expires (~10 min) so an
abandoned round-trip can never silently apply a highlight during a much later
sign-in. It is intent, not highlight data (highlights stay server-only,
ADR-001); discarded on decline, cancel, failure, or successful apply.

## Opted-in Bible read

An opted-in Bible read is a GET under `/v1/bibles/{id}` that honors Cache-Control remaining lifetime.

The list is version, book, books, chapter, chapters, verse, verses, and passage.

Highlights, GET `/v1/bibles` versions list, VOTD, languages, organizations, theme, and auth stay out.

The how and where live in `docs/bible-read-cache.md`.
ADR 0006 is the why.
This is not YPE-5262.

## Remaining lifetime

Remaining lifetime is `max-age` minus `Age`, in milliseconds, on CachePolicy as `remainingMs`.

Missing max-age is 7 days.
Missing Age is 0.
`no-cache` / `no-store` set remainingMs 0.

## CachePolicy

CachePolicy is the parsed Cache-Control result from `parseCachePolicy`.

Fields include `remainingMs` and `allowsCaching`.

## getWithPolicy

getWithPolicy is the `ApiClient` GET that returns `{ data, policy }`.

`ApiClient.get` stays body-only.

## Memory-only

Memory-only means QueryClient lives in process memory.
One QueryClient per YouVersionProvider.

Expo DOM WebViews do not share this memory.

Do not use this layer as the React Native Expo disk cache.
Do not persist QueryClient.
Do not share one QueryClient across Expo WebViews.
Do not wrap window.fetch in the Web SDK to close YPE-5262.

## Remount

A remount is a new hook mount after the previous observer unmounted, under the same YouVersionProvider.

## Still-mounted observer

A still-mounted observer is a hook that stayed mounted after remaining lifetime ended.
9 changes: 9 additions & 0 deletions docs/adr/0006-tanstack-query-read-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ Amended: 2026-09-01

Accepted

## How to navigate

This ADR is the why, not the file map.
Read `docs/bible-read-cache.md` for owners, remount contracts, and files.
Do not use this layer as the React Native Expo disk cache.
Do not persist QueryClient.
Do not share one QueryClient across Expo WebViews.
Do not wrap window.fetch in the Web SDK to close YPE-5262.

## Context

`useApiData` was a hand-rolled effect: every mount and every dep change refetched, nothing was cached, and disabling a query threw its data away. Revisiting a chapter re-blanked the reader; account data was cleared imperatively on auth changes, which is easy to get wrong once and leak one user's highlights to another. The hook-level call shape is documented in `packages/hooks/AGENTS.md`.
Expand Down
113 changes: 113 additions & 0 deletions docs/bible-read-cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Bible read cache

Opted-in Bible reads honor Cache-Control remaining lifetime in a memory-only QueryClient.

This map is the how and where. ADR 0006 is the why. This is not YPE-5262.

## Tower

1. The Platform API returns Cache-Control and Age on GET bodies.
2. `parseCachePolicy` in core turns headers into CachePolicy (`remainingMs`, `allowsCaching`).
Missing max-age is 7 days.
Missing Age is 0.
`no-cache` / `no-store` set remainingMs 0.
Parse errors fail open to no cache.
3. `ApiClient.get` stays body-only.
`getWithPolicy` returns `{ data, policy }`.
4. BibleClient methods for an opted-in Bible read call getWithPolicy.
Public body-only wrappers stay.
5. Opted-in Bible read hooks pass getWithPolicy into useApiData.
useApiData writes remainingMs onto that query's staleTime and gcTime after a successful first fetch.
The write covers the live query, not only setQueryDefaults.
6. Public hook shape stays `{ data, loading, error, refetch }`.
No TanStack types leave the package.
7. QueryClient is private, memory-only, and one per YouVersionProvider.
Expo DOM WebViews do not share this memory.
This is not YPE-5262.

## Opt-in

An opted-in Bible read is GET `/v1/bibles/{id}` or a subpath of that id:

- version
- book
- books
- chapter
- chapters
- verse
- verses
- passage

These stay out:

- highlights
- GET `/v1/bibles` versions list
- VOTD
- languages
- organizations
- theme
- auth

## Contracts

| Event | Result |
| --- | --- |
| remount inside remaining lifetime | cache hit, no fetch |
| remount after remaining lifetime | miss, fetch |
| still-mounted observer after expiry | keep showing the body (focus refetch is off) |
| no-cache / no-store | write while mounted with gcTime 0 so the still-mounted observer can keep the body; remount misses |
| refetch() | always fetches |
| default hooks | staleTime 0, gcTime 5 minutes |

## Fence

Do not use this layer as the React Native Expo disk cache.
Do not persist QueryClient.
Do not share one QueryClient across Expo WebViews.
Do not wrap window.fetch in the Web SDK to close YPE-5262.

## File ownership

| Package | Owns |
| --- | --- |
| core | `parse-cache-policy.ts`, client getWithPolicy, bible.ts `*WithPolicy` methods, MSW tests for HTTP+parse |
| hooks | useApiData envelope, opted-in hook method swap, remount tests |
| ui | no cache policy code |
| Expo SDK | out of this repo |

Hooks do not re-test parse tables.

### Files

core:

- `packages/core/src/parse-cache-policy.ts`
- `packages/core/src/parse-cache-policy.test.ts` (Kotlin/Swift parity table)
- `packages/core/src/client.ts` (`get`, `getWithPolicy`)
- `packages/core/src/bible.ts` (`*WithPolicy` twins)
- `packages/core/src/__tests__/client.test.ts`
- `packages/core/src/__tests__/bible.test.ts`

hooks:

- `packages/hooks/src/useApiData.ts`
- `packages/hooks/src/useApiData.test.tsx`
- `packages/hooks/src/useVersion.ts` and `useVersion.test.tsx`
- `packages/hooks/src/usePassage.ts` and `usePassage.test.tsx`
- other opted-in Bible read hooks: `useBook.ts`, `useBooks.ts`, `useChapter.ts`, `useChapters.ts`, `useVerse.ts`, `useVerses.ts`
- `packages/hooks/src/context/YouVersionProvider.tsx` (one memory-only QueryClient)
- `packages/hooks/src/internal/queryClientDefaults.ts`

ui has no cache policy code.

Kotlin and Swift parse the same way. The parity table is `packages/core/src/parse-cache-policy.test.ts`.

## Agent control

If you add a new Bible GET hook, opt it in only when the path is under `/v1/bibles/{id}`.

If you change `parseCachePolicy`, update the Kotlin/Swift parity table in `parse-cache-policy.test.ts` and this map.

If you change remount behavior, update useApiData / useVersion / usePassage remount tests and this map.

ADR 0006 stays the why. This map is the how and where.
2 changes: 2 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Do not use "integration" as a style term — choose by capability:

core owns HTTP+Zod+MSW; hooks own React state against stubbed clients; UI owns user-visible behavior against stubbed hooks/providers. Stub UI hook results with `YouVersionContext.hookOverrides` (see `packages/ui/src/test/hook-overrides.tsx`), not `vi.mock`. Do not re-test a lower package's contract unless the bug is at the boundary. Rare vertical smokes (e.g. highlight auth) may climb one rung for critical journeys.

Remount tests for opted-in Bible reads live in hooks and must stay. Core owns the `parseCachePolicy` table. See `docs/bible-read-cache.md`.

## Scope

Bind on new/edited tests. When touching a file, bend the cases you edit toward this style — no mass rewrite of untouched suites.
Expand Down
17 changes: 17 additions & 0 deletions packages/core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,23 @@ See `docs/adding-a-core-endpoint.md`.
- `YouVersionAPI` is a separate static header helper, not a base client. Do not
build a new client on it.

## CachePolicy and getWithPolicy

`parseCachePolicy` turns Cache-Control and Age into CachePolicy.

`ApiClient.get` stays body-only. `getWithPolicy` returns `{ data, policy }`.

A new Bible GET under `/v1/bibles/{id}` gets a `*WithPolicy` twin.

Public body-only wrappers stay. Do not put TanStack here.

The how and where live in `docs/bible-read-cache.md`.

Do not use this layer as the React Native Expo disk cache.
Do not persist QueryClient.
Do not share one QueryClient across Expo WebViews.
Do not wrap window.fetch in the Web SDK to close YPE-5262.

## CONVENTIONS
- Schema-first: All types defined in schemas/*.ts using Zod
- Zero React: Pure TypeScript, no React dependencies
Expand Down
18 changes: 14 additions & 4 deletions packages/hooks/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,20 @@ the pnpm `minimumReleaseAge` window):
- Cache is memory-only; `refetch` performs exact query invalidation. Writes
stay outside this layer (the highlights machine owns them) and refresh via
`refetch` after the write.
- Opted-in Bible reads (version, book, books, chapter, chapters, verse,
verses, passage) may serve from memory until remaining Cache-Control
lifetime ends. Remount after expiry fetches. Highlights, the versions
list, and VOTD keep `staleTime: 0` and revalidate on remount.
- Opted-in Bible read remount contracts live in `docs/bible-read-cache.md`.

| Event | Result |
| --- | --- |
| remount inside remaining lifetime | cache hit, no fetch |
| remount after remaining lifetime | miss, fetch |
| still-mounted observer after expiry | keep showing the body |
| no-cache / no-store | write while mounted with gcTime 0; remount misses |
| refetch() | always fetches |

- Do not use this layer as the React Native Expo disk cache.
- Do not persist QueryClient.
- Do not share one QueryClient across Expo WebViews.
- Do not wrap window.fetch in the Web SDK to close YPE-5262.
- Design decisions: `docs/adr/0006-tanstack-query-read-layer.md`.

## CONVENTIONS
Expand Down
Loading