Skip to content

fix(ios): SQLite transactions, credential trim, onboarding nav icon, safe-area - #1130

Merged
pkmetski merged 8 commits into
mainfrom
pkmetski/ios-perf-auth-onboarding-fixes
Sep 29, 2026
Merged

pkmetski merged 8 commits into
mainfrom
pkmetski/ios-perf-auth-onboarding-fixes

Conversation

@pkmetski

Copy link
Copy Markdown
Owner

Changes

fix(ios): wrap batch DAO writes in explicit SQLite transactions — 20+ second library load

On iOS, SQLite operates without a shared Room @Transaction wrapper.
Every driver.execute() in a loop triggered its own WAL fsync (~5-15 ms on NAND), turning a 500-book library refresh into a 20+ second hang.

Fix: new SqlDriver.withTransaction extension (BEGIN IMMEDIATE / COMMIT / ROLLBACK) wraps the full batch in a single fsync. Applied to IosLibraryItemDao.replaceAllForLibrary, IosSeriesDao.replaceAllForLibrary, and IosCollectionDao.replaceAllForLibrary. IosLibraryRefresherImpl batches per-item progress updates via the new batchUpdateReadingProgressFromServer DAO method.

Regression coverage: IosLibraryItemDaoTest (10 tests in core:database iosSimulatorArm64Test).

fix(add-source): trim whitespace from username and password before authenticating

The URL field was already trimmed; credentials were not. iOS can insert invisible trailing whitespace (autocorrect, physical keyboard layout mismatch), causing "test " to reach the server as the password and return 401. Fix: .trim() on username and password in doAuthenticate() and connectWebdav().

Regression test: AddSourceViewModelTest.onConnect trims leading and trailing whitespace from username and password before authenticating (androidHostTest).

iOS path: AddSourceViewModel is commonMain; iOS exercises the same trim code path. No separate iOS test needed per the ViewModel-with-Room-coupled-constructor exception — but the trim itself is pure String.trim() in shared code.

fix(source-ui): hide back navigation icon during first-run onboarding

During first-run onboarding (no source configured) the back arrow was shown but had nowhere to go. SourceTypePickerScreen and AddSourceScreen gain showNavigationIcon (default true). SourceOnboardingHost gains canNavigateBack (default true) forwarded to both screens. HomeScreen passes canNavigateBack = false. On Android, SourceNavGraph passes showNavigationIcon = cameFromSettings.

Tests:

  • Android: SourceTypePickerScreenTest.backButton_hiddenWhenShowNavigationIconFalse (androidTest)
  • iOS: OnboardingNavIconTests.testBackButtonAbsentOnPickerDuringOnboarding + testBackButtonAbsentOnAddSourceDuringOnboarding (iosAppTests target)

fix(ios): extend safe-area inset to all edges in ContentView

ContentView previously only excluded the bottom safe-area edge. Changed to .ignoresSafeArea(.all) so the Compose layer fills the full screen including the status-bar region, matching Android window-insets behaviour.

fix(ios-db): always invalidate after replaceAllForLibrary; fix INSERT column count

Two bugs found in code review:

  1. IosCollectionDao and IosSeriesDao gated invalidator.invalidate() on isNotEmpty(). A server refresh that removes ALL series/collections left deleted rows visible in the library indefinitely. Fix: invalidate unconditionally.

  2. insertOrIgnoreRaw (and older insertOrIgnore) passed 24 as the SQLite parameter count, but ALL_COLS/PLACEHOLDERS has 25 columns. The 25th binding (progressServerUpdatedAt) was silently dropped, storing 0 on every newly inserted row. Fix: pass 25 in both call sites.


iOS call path

  • Library load: IosLibraryRefresherImpl → IosLibraryItemDao.replaceAllForLibrary → SqlDriver.withTransaction
  • Auth trim: AddSourceViewModel.doAuthenticate (commonMain) → username.trim() / password.trim() → AbsSourceAdapter.authenticate
  • Onboarding nav: HomeScreen → SourceOnboardingHost(canNavigateBack=false) → SourceTypePickerScreen(showNavigationIcon=false) / AddSourceScreen(showNavigationIcon=false)

🤖 Generated with Claude Code

…x 20+ second library load

On iOS every driver.execute() call without an explicit transaction is an implicit
auto-commit that triggers its own WAL fsync (~5-15 ms on NAND flash). For a 500-book
ABS library this produced 1000+ fsyncs (500 INSERT OR IGNORE + 500 UPDATE from
replaceAllForLibrary, plus up to 500 more from the per-item progress update loop)
totalling 20+ seconds before the spinner cleared.

Android is unaffected because Room's @transaction wraps the default replaceAllForLibrary
implementation in a single BEGIN/COMMIT automatically.

Fix:
- Add IosTransaction.kt with a SqlDriver.withTransaction() suspend extension that wraps
  a block in BEGIN IMMEDIATE / COMMIT / ROLLBACK.
- Override replaceAllForLibrary in IosLibraryItemDao, IosSeriesDao, and IosCollectionDao
  to issue all SQL inside one transaction and fire invalidator.invalidate() only once at
  the end (instead of once per row for updateMetadata).
- Add LibraryItemDao.batchUpdateReadingProgressFromServer (default: loops + @transaction
  for Android; iOS override: single explicit transaction) and replace the per-item loop
  in IosLibraryRefresherImpl with a single batch call.
- Add IosLibraryItemDaoTest covering replaceAllForLibrary correctness (inserts, stale-
  item deletion, metadata update, local-progress preservation) and batch progress update
  correctness (multi-item update, last-write-wins staleness guard).

Expected improvement: 500-book refresh ~200 ms on iOS (down from 20+ s).
…thenticating

A physical keyboard on iOS (or autocorrect on any platform) can insert a
trailing space into a text field, causing "test " to be sent instead of
"test" and returning 401 from the server. The URL field was already trimmed;
the credentials were not.

Fix: call .trim() on username and password in doAuthenticate() and
connectWebdav() before passing them to the authenticator.

Regression test: AddSourceViewModelTest asserts that leading/trailing
whitespace is stripped from both fields before the authenticate() call.
When the app launches with no source configured the SourceOnboardingHost
is shown at root — there is nowhere to navigate back to, so showing the
back arrow is confusing and non-functional.

AddSourceScreen and SourceTypePickerScreen gain a showNavigationIcon
parameter (default true, so all existing callers are unaffected).
SourceOnboardingHost gains canNavigateBack (default true) and forwards
it to both screens. HomeScreen passes canNavigateBack=false when
showing the onboarding flow.

On Android, SourceNavGraph passes showNavigationIcon=cameFromSettings
to both screens, hiding the icon on first-run and showing it when
reached from Settings.
Previously only the bottom edge was excluded from safe-area insets.
Switching to .ignoresSafeArea(.all) lets the Compose layer fill the
full screen including the status-bar region, matching the Android
window-insets behaviour.
…g onboarding

Android: SourceTypePickerScreenTest adds backButton_shownByDefault and
backButton_hiddenWhenShowNavigationIconFalse to lock the regression that
the nav_back button disappears when canNavigateBack=false is passed
through SourceOnboardingHost.

iOS: OnboardingNavIconTests (iosAppTests target) drives the same claim
through XCUIApplication — verifies nav_back is absent on both the
picker and AddSource screens during first-run onboarding (app launched
with --RIFFLE_RESET_FOR_TESTS and no seeded source).
… column count

Two bugs surfaced in code review:

1. IosCollectionDao and IosSeriesDao gated invalidator.invalidate() on
   `isNotEmpty()`, so a refresh that removes ALL series/collections from
   the server left deleted rows visible in the library indefinitely.
   Fix: invalidate unconditionally after every replaceAllForLibrary call
   (the transaction already ran the DELETEs, so the UI must be notified).

2. insertOrIgnoreRaw (and the older insertOrIgnore) passed 24 as the
   parameter count to SqlDriver.execute() but ALL_COLS/PLACEHOLDERS have
   25 columns. The 25th binding (progressServerUpdatedAt) was silently
   dropped, storing 0 for that column on every newly inserted row.
   Fix: pass 25 in both call sites.
@pkmetski
pkmetski merged commit d703bff into main Sep 29, 2026
13 checks passed
@pkmetski
pkmetski deleted the pkmetski/ios-perf-auth-onboarding-fixes branch September 29, 2026 18:52
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.

1 participant