refactor: coordinator boundary review + AppState façade (#50 + #48)#58
refactor: coordinator boundary review + AppState façade (#50 + #48)#58variablefate merged 13 commits intomainfrom
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
PROCESS CORRECTION for agent a7cd1ce30141c2ef4 — please acknowledge and adjust your approach: Your finish line is marking the draft PR ready for review, NOT merging it. Specifically:
Everything else in your instructions stands. Continue your work. |
…sView use AppState façade
Add publishDriversList(), requestDriverKeyRefresh(driverPubkey:), and checkForStaleDriverKeys() to the AppState Drivers façade. Update AddDriverSheet, DriversTab, and RideTab to use these instead of reaching through appState.rideCoordinator directly. RideTab also switches to appState.isRelayConnected() instead of appState.relayManager. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When hasFollowedDrivers is false the view previously rendered nothing, leaving new users with a blank RideTab. Add a "No Drivers Added" empty state with a CTA that navigates to the Drivers tab, matching the pattern used in DriversTab's own empty state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SavedLocationsView.swift, ConnectivityIndicator.swift, and RideTab.swift were all modified as part of Phase B but were omitted from the ADR's Affected Files list. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code reviewFound 4 issues (3 fixed, 1 discarded after verification): Surfaced findings and disposition: Fixed — 9f23c7c: Incomplete façade — Three call sites still reached through roadflare-ios/RoadFlare/RoadFlare/Views/Drivers/AddDriverSheet.swift Lines 303 to 316 in 449c466 roadflare-ios/RoadFlare/RoadFlare/Views/Drivers/DriversTab.swift Lines 158 to 161 in 449c466 roadflare-ios/RoadFlare/RoadFlare/Views/Ride/RideTab.swift Lines 92 to 95 in 449c466 Fixed — 9a8951f:
roadflare-ios/RoadFlare/RoadFlare/Views/Ride/RideRequestView.swift Lines 41 to 45 in 449c466 Fixed — 33c3bde: ADR-0011 Affected Files list incomplete
Discarded — Flagged because a prior commit (5e5d133) explicitly separated 🤖 Generated with Claude Code If this code review was useful, please react with 👍. Otherwise, react with 👎. |
… gaps - ActiveRideView: replace driversRepository?.cachedDriverName with appState.driverDisplayName(pubkey:) — last remaining driversRepository bypass in a view - AppState: add allPaymentMethodNames to Settings facade; update RideRequestView to use it instead of appState.settings directly - AppState: document addDriver() publish asymmetry — unlike removeDriver and updateDriverNote, callers must publish separately - AppState: clarify Settings MARK comment to reflect write access still goes through appState.settings directly - ADR-0011: add ActiveRideView.swift to Affected Files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code review — second pass4 findings this pass; all fixed in bec34a0. 3 others discarded after verification. Fixed —
roadflare-ios/RoadFlare/RoadFlare/Views/Ride/ActiveRideView.swift Lines 22 to 26 in 33c3bde Fixed — The Settings façade block added read-only helpers for badge counts and roadflare-ios/RoadFlare/RoadFlare/Views/Ride/RideRequestView.swift Lines 196 to 200 in 33c3bde Fixed —
roadflare-ios/RoadFlare/RoadFlareCore/ViewModels/AppState.swift Lines 709 to 718 in 33c3bde Fixed — ADR-0011 Affected Files and Settings MARK still stale
Discarded findings:
What this pass caught that the first missed: The first pass focused on files in the diff and noted 🤖 Generated with Claude Code If this code review was useful, please react with 👍. Otherwise, react with 👎. |
DriverDetailSheet.canRequestRide, RideRequestView.onlineDrivers, and DriversTab.isOnline all accepted hasKey + online status without checking isDriverKeyStale. A driver broadcasting "online" with a stale RoadFlare key would surface as bookable; the rider's encrypted offer would not be decryptable by the driver and the request would silently fail. The SDK already blocks ping via canPingDriver returning .ineligible when staleKeyPubkeys contains the driver, but the ride-offer path had no equivalent view-layer guard. Now all three predicates also require !isDriverKeyStale(pubkey:), matching the "Key Outdated" visual state DriverCard already shows. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SettingsTab: use appState.profileName and appState.paymentMethodCount instead of reading appState.settings.* directly (5 sites). These façade properties were added in this PR but the file's own reads weren't migrated. - DriversTab: remove stale sentence in pingDriver comment that referenced appState.driversRepository — no such access exists in the function after the façade migration. - ADR-0011: drop inaccurate "~650 LOC" figure for AppState (the file is larger after the Phase B façade additions that this same PR adds). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code review — third pass4 findings; 1 is a real user-facing bug the first two passes missed. Fixed in 0b2e519 and 4cdf7b7. Fixed (bug) — Stale-keyed drivers were bookable from the ride-request flow
PR #59's review surfaced the same defect in the new presentation-layer types; the root cause lives here. roadflare-ios/RoadFlare/RoadFlare/Views/Drivers/DriverDetailSheet.swift Lines 103 to 107 in bec34a0 roadflare-ios/RoadFlare/RoadFlare/Views/Ride/RideRequestView.swift Lines 24 to 28 in bec34a0 roadflare-ios/RoadFlare/RoadFlare/Views/Drivers/DriversTab.swift Lines 366 to 368 in bec34a0 Fixed — 5 reads still went through roadflare-ios/RoadFlare/RoadFlare/Views/Settings/SettingsTab.swift Lines 45 to 49 in bec34a0 roadflare-ios/RoadFlare/RoadFlare/Views/Settings/SettingsTab.swift Lines 110 to 112 in bec34a0 Fixed — Stale comment in Comment at line 131 claimed roadflare-ios/RoadFlare/RoadFlare/Views/Drivers/DriversTab.swift Lines 127 to 132 in bec34a0 Fixed — ADR-0011 self-contradictory LOC claim ADR describes AppState as "~650 LOC" but the file is 870 lines after this PR's façade additions. Replaced the figure with prose that stays accurate as the file evolves. roadflare-ios/decisions/0011-coordinator-boundary.md Lines 26 to 28 in bec34a0 Discarded after verification:
What this pass caught that earlier passes missed: The stale-key bug is the substantive one. The first two passes audited the façade by walking each view's diff for SDK-type accesses and never examined the views' own business-logic predicates. 🤖 Generated with Claude Code If this code review was useful, please react with 👍. Otherwise, react with 👎. |
The stale-key fix from the third review pass inlined the ride-eligibility check into three separate view predicates (DriverDetailSheet.canRequestRide, RideRequestView.onlineDrivers, DriversTab.isOnline). Future regressions in any one predicate would not be caught by the test suite, and the three copies could drift apart. Extract the predicate to FollowedDriversRepository.canRequestRide(_:), parallel to the existing canPingDriver(_:). The SDK method takes a lock, looks up the driver in the repo (not the caller's snapshot), and checks: hasKey, !stale, status == "online". Expose appState.canRequestRide(_:) as the façade forwarder. All three view predicates now call the single source of truth. Adds 8 CanRequestRideTests covering: unknown driver, missing key, stale key, offline, on-ride, online with key, and the two stale-caller-snapshot cases that CanPingDriverTests covers symmetrically. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code review — fourth pass (convergence check on e68ec8c)No new findings worth acting on in this PR. The canRequestRide refactor + tests converges cleanly:
Two forward-looking observations (not flagged for fix in this PR — flagging here for visibility): 1. Send-time preflight gap on The ping path has UI-side The UI fix closes the user-visible case; this would close the race window. Scope is adjacent to this PR, not in it — suitable as a follow-up issue. Pattern precedent is commit bd2b3f1, which introduced 2. PR #59 presentation-type factories inline the predicate PR #59's Branch is ready to merge. This pass found no issues in #58's own scope. 🤖 Generated with Claude Code If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Pass-1 review fixes: - restoreRideState() runs in RideCoordinator.init, before restoreLiveSubscriptions() starts the Kind 30173 stream — the cache is always empty at restore time, so cold-started mid-ride sessions permanently saw a nil snapshot and fell back to Kind 0. Add a narrow onDriverVehicleUpdate hook on LocationCoordinator that lets RideCoordinator opportunistically adopt the first observed vehicle for the active driver, then lock for the rest of the ride. Two new tests pin first-arrival adoption + lock-after-first and ignored events for unrelated drivers / non-active stages. - Update activeRideVehicle docstring to accurately describe both capture paths and the cold-start trade-off. - Annotate prepareForIdentityReplacement step 4 so the reader sees that clearAll() now also zeroes the new driverVehicles cache. - Add ADR-0015 documenting the Kind 30173 subscription pattern, the overwrite-only cache, the snapshot semantics + first-arrival recovery, and why this stays in-memory rather than going through PersistedRideState. Adjacent debt (PR #58/#61 inline canRequestRide drift) tracked at issue #94 to keep PR scope focused on issue #91. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(roadflare): subscribe to Kind 30173 for live driver vehicles (#91) Riders couldn't see driver vehicle info because iOS only read Kind 0 (which Drivestr doesn't reliably re-publish on vehicle swap). Subscribe to Kind 30173 DriverAvailabilityEvent for followed drivers, cache with overwrite-only semantics, and snapshot at ride acceptance so mid-trip vehicle swaps don't mutate the active ride view. - SDK: VehicleInfo + DriverAvailabilityEventData models, parseDriverAvailability parser, NostrFilter.driverAvailability(driverPubkeys:), and a no-merge driverVehicles cache on FollowedDriversRepository. - App: LocationCoordinator gains a Kind 30173 subscription mirroring the Kind 30014 pattern; AppState exposes restartDriverAvailabilitySubscription() alongside restartKeyShareSubscription, called at every followed-drivers mutation site (add/remove/follow-notify/refresh). - RideCoordinator captures activeRideVehicle on .waitingForAcceptance → .driverAccepted and clears at terminal; ActiveRideView prefers the snapshot, falls back to Kind 0. - Presentation: DriverDetailViewState/DriverListItem prefer the live cache with the Kind 0 profile as a fallback. AppState+Presentation threads driverVehicles through. - Tests: parser shape (full / partial / offline / wrong-kind / malformed), repo overwrite + multi-vehicle + multi-driver + cleanup, snapshot capture + lock at acceptance + nil-when-cache-empty, presentation precedence. Closes #91 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(rider): adopt first Kind 30173 event for restored mid-ride snapshot Pass-1 review fixes: - restoreRideState() runs in RideCoordinator.init, before restoreLiveSubscriptions() starts the Kind 30173 stream — the cache is always empty at restore time, so cold-started mid-ride sessions permanently saw a nil snapshot and fell back to Kind 0. Add a narrow onDriverVehicleUpdate hook on LocationCoordinator that lets RideCoordinator opportunistically adopt the first observed vehicle for the active driver, then lock for the rest of the ride. Two new tests pin first-arrival adoption + lock-after-first and ignored events for unrelated drivers / non-active stages. - Update activeRideVehicle docstring to accurately describe both capture paths and the cold-start trade-off. - Annotate prepareForIdentityReplacement step 4 so the reader sees that clearAll() now also zeroes the new driverVehicles cache. - Add ADR-0015 documenting the Kind 30173 subscription pattern, the overwrite-only cache, the snapshot semantics + first-arrival recovery, and why this stays in-memory rather than going through PersistedRideState. Adjacent debt (PR #58/#61 inline canRequestRide drift) tracked at issue #94 to keep PR scope focused on issue #91. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(rider): pass-2 review polish — ADR clarifications + callback rationale - ADR-0015: enumerate all three populate paths for activeRideVehicle (acceptance transition, restoreRideState direct read, first-arrival adoption). Match the precision precedent set by ADR-0012. - ADR-0015: defend the closure-callback choice against the a88d1b7 (PR #38) precedent that removed onFavoritesChanged in favor of @observable reactive observation — first-arrival adoption needs imperative locking semantics that withObservationTracking does not naturally express. - ADR-0015: hedge the future-consumers claim and call out that onDriverVehicleUpdate is package-internal (no public modifier). - ADR-0015: note that this ADR supersedes ADR-0011's "two subscriptions" prose for LocationCoordinator (now three). - LocationCoordinator: doc onDriverVehicleUpdate's intentional non-Sendable (covered by @mainactor isolation, asymmetric with SDK callbacks for a reason) and its intentional fire-even-when-cache-write-skipped behavior (the snapshot represents the agreed vehicle, meaningful even after an unfollow during an active ride). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(rider): sharpen vehicle-snapshot lock + correct onDriverVehicleUpdate doc Pass-3 review entropy fixes (sub-threshold but real): - vehicleSnapshotDoesNotUpdateAfterAcceptance previously only mutated the repo cache after capture, which proved snapshot-as-captured-value immutability but never exercised the actual production path (LocationCoordinator -> onDriverVehicleUpdate -> adoptVehicleIfNeeded). A regression that removed `guard activeRideVehicle == nil` from adoptVehicleIfNeeded would not have been caught here. Now the test mirrors the production sequence (cache write THEN adoptVehicleIfNeeded call), so it actually pins the lock guard for the bug it's named after. - onDriverVehicleUpdate doc previously claimed "the closure cannot escape" to justify the missing @sendable. That's factually wrong — stored optional closures are by definition @escaping. The real safety property is actor isolation, which itself depends on the call site being an unstructured `Task { }` (inheriting @mainactor) rather than Task.detached. Rewrite the doc to enumerate both load-bearing conditions and call out exactly what a Task.detached refactor would break, so a future contributor doesn't silently introduce a data race with RideCoordinator.activeRideVehicle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(rider): pin AppState presentation wiring of driverVehicles cache Adds two integration-style tests that exercise the actual AppState API (`driverListItems()` and `driverDetailViewState(pubkey:)`) end-to-end with both a Kind 0 profile and a live Kind 30173 cache entry, asserting the live cache wins. Without these, the `vehicle:` parameter on the factory has a nil default and a future refactor that drops `vehicle:` from the AppState+Presentation call sites would silently fall back to Kind 0 — the exact bug #91 fixed — without any existing test catching it. The factory unit tests pass `vehicle:` explicitly so they don't exercise the AppState wiring. Pre-existing empty-pubkeys subscription race in all three managed subscriptions filed as #96 (out of scope for #91; needs a uniform fix). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: variablefate <variablefate@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
AppState/RoadFlareCoreso SwiftUI views no longer bypass the façade to reach SDK repositories directly (closes Review AppState facade boundary over SDK repositories/services #48)Scope
#50 — Coordinator boundary review (ADR-0011):
RideCoordinator,ChatCoordinator,AppState,SyncCoordinator,LocationCoordinator#48 — AppState façade:
AppStatecovering: connectivity (isRelayConnected()), drivers (read + follow/unfollow/note actions), ride history (read + delete + backup), saved locations (read + pin/save/remove/clear), settings (badge counts)DriversTab,DriverDetailSheet,AddDriverSheet,RideRequestView,HistoryTab,SettingsTab,SavedLocationsView,ConnectivityIndicatorimport RidestrSDKremains in refactored views because they render SDK value types (FollowedDriver,RideHistoryEntry,CachedDriverLocation, etc.) — eliminating those imports is the job of Review view-layer coupling to SDK models and add app presentation models #49 (PR refactor: app presentation types for view-layer decoupling (#49) #59, presentation types)Merge order
This PR merges before #59 (presentation types), which rebases onto it.
Test plan
xcodebuildclean build passes — confirmed on iPhone 17 simulatordecisions/Closes #50
Closes #48
🤖 Generated with Claude Code