Spun out of PR #76 review. Score-75 architectural concern that didn't meet that PR's scope bar.
Context
Two per-driver cooldown maps live in AppState:
pingCooldowns: [String: Date] — 10-minute window for Kind 3189 driver-ping (ADR-0009).
keyRefreshCooldowns: [String: Date] — 60-second window for Kind 3188 stale-ack (ADR-0013).
Both gate user-tap surfaces and mirror cross-client protocol limits used by the Android equivalents (drivestr/.../MainActivity.kt:424-460 for Kind 3186 republish, rider-app/.../RoadflareTab.kt:109-178 for the refresh map). CLAUDE.md says: "Sync domain logic (publish + state machines + resolution) belongs in the SDK."
What's wrong
Both cooldowns are protocol-level rate-limit state that just happen to be triggered from the app UI. Keeping them in AppState means:
- Cross-client divergence is easy: tweaking the iOS window doesn't touch Android's, and vice versa. The 60s figure for keyRefreshCooldownSeconds is a documented copy of Android's value with no compile-time linkage.
- The SDK's own "best-effort" callers (e.g.
LocationSyncCoordinator.checkForStaleKeys) bypass any rate limit, so a sweep + a user tap can fire two stale-acks within the window from the same client.
- Future surfaces that want to publish a Kind 3188 (e.g. a debug tool, an alternate flow) would have to either reach into AppState or re-implement the cooldown.
Proposed work
Move both cooldown maps and their _Outcome enums into the SDK, owned by their respective coordinators (LocationSyncCoordinator for key-refresh, the ping coordinator for driver-ping). AppState becomes a thin façade that surfaces the outcomes to SwiftUI.
This is a 2-3-file refactor, well-tested precedent for both cooldowns, and would tighten the SDK-vs-app split .claude/CLAUDE.md flagged.
Out of scope for #72 because it would also require moving the established pingCooldowns pattern, expanding the surface beyond a stale-key bug fix.
Spun out of PR #76 review. Score-75 architectural concern that didn't meet that PR's scope bar.
Context
Two per-driver cooldown maps live in
AppState:pingCooldowns: [String: Date]— 10-minute window for Kind 3189 driver-ping (ADR-0009).keyRefreshCooldowns: [String: Date]— 60-second window for Kind 3188 stale-ack (ADR-0013).Both gate user-tap surfaces and mirror cross-client protocol limits used by the Android equivalents (
drivestr/.../MainActivity.kt:424-460for Kind 3186 republish,rider-app/.../RoadflareTab.kt:109-178for the refresh map). CLAUDE.md says: "Sync domain logic (publish + state machines + resolution) belongs in the SDK."What's wrong
Both cooldowns are protocol-level rate-limit state that just happen to be triggered from the app UI. Keeping them in
AppStatemeans:LocationSyncCoordinator.checkForStaleKeys) bypass any rate limit, so a sweep + a user tap can fire two stale-acks within the window from the same client.Proposed work
Move both cooldown maps and their
_Outcomeenums into the SDK, owned by their respective coordinators (LocationSyncCoordinatorfor key-refresh, the ping coordinator for driver-ping). AppState becomes a thin façade that surfaces the outcomes to SwiftUI.This is a 2-3-file refactor, well-tested precedent for both cooldowns, and would tighten the SDK-vs-app split .claude/CLAUDE.md flagged.
Out of scope for #72 because it would also require moving the established
pingCooldownspattern, expanding the surface beyond a stale-key bug fix.