Skip to content

Native iOS app: offline reading - #139

Merged
Programcode merged 16 commits into
feat/native-iosfrom
native-ios/07a-offline-read
Oct 14, 2026
Merged

Programcode merged 16 commits into
feat/native-iosfrom
native-ios/07a-offline-read

Conversation

@Programcode

Copy link
Copy Markdown
Owner

Offline reading for the iOS app. Debates, Library sources, saved results, flowcharts, chats and judged Sparring rounds stay readable without a connection. AI and saving wait for the network, as the plan says.

How it works

  • The cache. offline-cache.ts keeps a copy of each read in AsyncStorage under the signed-in user's id. These modules read through it: debates.ts, sources.ts, results.ts, flowcharts.ts, chats.ts and sparring.ts.
  • Network first. A read asks the network first and saves the answer. When the read fails, or expo-network says the device is offline, the saved copy answers instead.
  • Stale answers. A read only replaces a copy when it started after the read that wrote it, so a slow answer never overwrites a newer one. A generation guard drops work started for an earlier account.
  • Account changes. Signing out or switching account wipes the copies through the same setSession path that clears session memory. A copy is checked against its owner again on every read.
  • Size limits. Each kind keeps a fixed number of copies (for example 40 per result kind and 20 Library lists), dropping the oldest first. A copy over 512k characters isn't kept.
  • Offline notice. It looks like OneOffNotice and says what's happening: "Offline: showing saved copies. AI and saving wait for the connection." The string is in en, es and hu.
  • Writes. There's no offline write queue. AI and saving fail with their existing translated network errors.

Checks (local; GitHub Actions is out of minutes)

At this head, run under the shared memory lock:

  • npm run verify: exit 0. Vitest 320/320 (including the new cache tests for account isolation and stale answers), collaboration 30/30, edge 333, build OK, smoke 30/30.
  • npm -w @debatelab/mobile run typecheck and npx eslint apps/mobile: exit 0.
  • Offline proof on staging (ONLY=offline, iPhone light en and dark hu, 15 screenshots):
    • Device offline: the Debate tab, Library and a source, compared with the online read.
    • Staging unreachable after a reload: the Debate tab, Library, a saved case and a flowchart.
    • Another account: copies planted for another user are removed and never shown.
    • Sign-out: 6 copies before it, 0 after.

Not provable here

  • real airplane mode on a phone
  • expo-network's native reachability (the web build uses navigator.onLine)
  • AsyncStorage size limits on a device
  • a real second account, since staging has one demo code; unit tests cover it

Codex review rounds follow as comments.

🤖 Generated with Claude Code

Programcode and others added 6 commits October 14, 2026 12:39
offline-cache.ts reads through the network and keeps a copy in
AsyncStorage under the user id; when the read fails, or expo-network
says the device is offline, the copy answers. Copies are removed when
the account changes, a slow answer never replaces a newer copy, and
each group keeps its most recently fetched entries only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Current Debate and debates list, counts and recent work, Library
sources with their text, saved cases, attacks, brainstorms, factoids,
flowcharts and chats now fall back to their saved copies. Deleting a
row drops its copy. The auth provider names the cache owner before it
adopts a session, next to the session memory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A notice leads the tabs, result documents and the flowchart while the
device is offline or a read fell back to a saved copy: "Offline:
showing saved copies. AI and saving wait for the connection." AI and
saving keep failing with their existing network errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ONLY=offline signs in to staging, reads the Debate tab, the Library and
a source, a saved case and a flowchart, then shows the same data under
the offline notice twice: with the browser offline while navigating
inside the app, and after a reload with every staging request failing.
It also plants another account's copies and checks they are removed
unseen, and that signing out removes every copy. The logout call is
answered locally, so the shared staging session stays valid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/07a-offline-read

# Conflicts:
#	apps/mobile/AGENTS.md
Sparring landed after the offline cache, so its saved rounds and the
Practice list now read through the same per-account cache, and a
deleted round drops its copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
debate-navigator Ready Ready Preview Oct 14, 2026 10:00pm UTC
debatelab-staging Ready Ready Preview Oct 14, 2026 10:00pm UTC

Copilot AI balanced review requested due to automatic review settings October 14, 2026 13:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository YAML (base), Organization UI (inherited)
  • Review profile: CHILL
  • Plan: Free
  • Run ID: 383f5660-2b87-496f-bc11-c48f02e276aa

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

@Programcode

Copy link
Copy Markdown
Owner Author

Codex review r1 at ad4ee4d (gpt-6.1-sol, high): 5 findings, all checked against the code and accepted; fixes in progress.

  1. Medium: switching or creating a debate leaves the cached Current Debate stale, so an offline restart restores the old one.
  2. Medium: saving over a map doesn't refresh its cached copy or supersede earlier reads.
  3. Medium: deleting a source (and results, flowcharts, chats, Sparring rounds) leaves cached lists with the deleted item.
  4. Medium: the fallback serves a cached copy for any error, including 401/permission refusals; it must only fall back on connectivity failures.
  5. Low: any successful read clears the offline notice while another screen still shows a cached copy.
    Codex found no cross-account leak: first-call ownership, same-account restart, different-account wipe, sign-out and read ordering all held.

Programcode and others added 4 commits October 14, 2026 13:56
A read used to answer from its saved copy for any error, so an expired
session, a policy refusal or a server error looked like being offline
and showed old data. The cached loaders now throw readFailed(error,
status), and cachedRead falls back only when supabase-js got no answer
(status 0: no connection, DNS failure, timeout). Everything else is
thrown as it is.

The saved-copies notice now counts each copy a screen shows, and a copy
stops counting only when that same data is read from the server again,
so a successful read elsewhere no longer hides it.

Also adds afterWrite and updateCopy, which the writers use in the next
commit; forgetCopy now goes through afterWrite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Writes left the saved copies as they were, so offline the app showed
the Current Debate before a switch, a map before it was saved over, and
Library lists, counts and recent work that still held deleted rows.

Every write that succeeds now calls afterWrite with the copies it
changed: one key, or the keys of a group that start with a prefix and
name a debate. Those copies are dropped, and reads of them that started
before the write cannot save their older answer. Where the new value is
known, updateCopy stores it instead: switching or creating a debate
updates the saved list and Current Debate, and saving a map stores the
map as saved. Deletes read back the row's debate so its lists drop too.
Judging a Sparring round and filing it in a debate drop the round lists
it appears in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Programcode

Copy link
Copy Markdown
Owner Author

Codex review r2 at a587dae (gpt-6.1-sol, high): r1's five findings verified fixed (switch/create updates, map-save sequencing, HTTP-error rejection, owner isolation). Three new findings, accepted; fix in progress:

  1. Medium (results.ts:283): a delete that returns no row (already deleted elsewhere) leaves cached lists and counts with the item. Same in sources, flowcharts, Sparring.
  2. Medium (debates.ts:101): in parallel reads, a connectivity failure in one query masks a 401/refusal in another, so the cached list is served instead of the auth error.
  3. Low (offline-cache.ts:197): a closed screen's key stays in the 'shown' set, so the offline notice can persist after reconnecting.

Programcode and others added 5 commits October 14, 2026 17:09
A delete that succeeds with no row (already deleted on the website) does
not name the debate that held the item, so the copies of every debate's
Library, maps, rounds, counts and recent lists of that kind are dropped
instead of none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A read built from several queries rejected with whichever failed first, so
one query that got no answer could hide another's 401 and serve the saved
copy. The debates list, a chat, the debate counts and the recent and
Prepare lists now wait for every query and throw the refusal when there is
one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The offline notice tracked shown copies by key only, so a copy kept the
notice up after reconnecting until that exact item was read again. Each
screen now reads with a CopyHolder mark: a newer load lets go of what its
older loads showed, a late read from a closed screen no longer counts, and
closing a screen, leaving a tab or clearing its data releases its copies.
The Flowchart screen shares its holder with the saved maps sheet that
opens maps for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Programcode

Copy link
Copy Markdown
Owner Author

Round-2 fixes: f7af0a0 (a delete that returns no row drops every list it may touch), 55433c9 (a server refusal wins over a missing answer in parallel reads, incl. loadChat), 7a45e57 (a saved copy stops counting once its screen closes or moves on). Tests for findings 1 and 2 fail without the fixes. Merged feat/native-ios (Sparring, dev-build prep, entitlements). Local at a846afc: npm run verify green (335 vitest, 30 smoke), mobile typecheck, eslint, knip pass. Codex r3 next.

readAll held a refusal until every other query settled, so a 401 behind
a query that never answered left the screen loading. A refusal now
rejects as soon as it arrives; only queries that got no answer wait for
the rest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Programcode

Copy link
Copy Markdown
Owner Author

Codex review r3 at a846afc (gpt-6.1-sol, high): r2's three fixes hold (delete invalidation and refusal preference passed probes; both merges clean). Two findings:

  1. Medium (offline-cache.ts:91): readAll held a known refusal until a stalled sibling query settled, leaving the screen loading. Fixed in 95313b1: a refusal rejects at once; only unanswered queries wait for the rest. New test fails without the fix.
  2. Low (use-result-document.ts:68, flowchart/index.tsx:103): reopening a document held in session memory after a fallback read doesn't restore the 'saved copies' notice. Dismissed: that path shows the app's own in-memory copy of a document opened this session, not a fallback read, and when the device is offline the notice shows from the network state alone; the only gap is the server-unreachable-while-device-online case on a reopen, which is informational.
    Local at 95313b1: npm run verify green (336 vitest, 30 smoke), mobile typecheck, eslint, knip pass. Codex r4 next.

@Programcode

Copy link
Copy Markdown
Owner Author

Codex review r4 at 95313b1 (gpt-6.1-sol, high): no findings. readAll ordering, empty input, multiple refusals, mixed failures, later rejections and typing checked; no caller depends on waiting after a refusal. Merging into feat/native-ios. Not provable here: real airplane mode on a phone, expo-network's native reachability and AsyncStorage's native limits.

@Programcode
Programcode merged commit db2c216 into feat/native-ios Oct 14, 2026
8 of 16 checks passed
@Programcode
Programcode deleted the native-ios/07a-offline-read branch October 14, 2026 22:00
Programcode added a commit that referenced this pull request Oct 14, 2026
Brings in Sparring (#138), development-build prep (#142), App Store
entitlements (#141) and offline reading (#139).

- The push migration becomes 20261014130000_push_tokens.sql so it sorts
  after 20261014123154_app_store_entitlements.sql.
- auth.tsx and _layout.tsx keep both the push wiring (release on sign-out,
  refresh on launch and language change) and setCacheOwner/watchNetwork.
- package-lock.json is feat's lock plus `npm install` for expo-notifications.
- app.config.ts is unchanged: #142 already strips aps-environment from
  development builds. The mobile guide now says so, and that real push needs
  a profile with push and an APNs key from Adam's Apple Developer account.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

2 active deployments
Preview – debatelab-staging — 95313b11 Deployed Oct 14, 2026 by vercel[bot]
Preview – debate-navigator — 95313b11 Deployed Oct 14, 2026 by vercel[bot]
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.

2 participants