Skip to content

Add bounded offline Wayfarer route persistence and reuse #261

Description

@stef-k

Problem

Authenticated Wayfarer routing can provide connected guidance, but transient results do not support offline reuse. The current five-minute cached_osrm_route preference is removed by the first #253 child and is not trustworthy for migration: it lacks server/provider/profile provenance and bounded durable ownership. Mobile needs a distinct production persistence contract for only storage-authorized validated Wayfarer routes.

Parent: #253. Coordinated release: stef-k/Wayfarer#505. Depends on the authenticated-routing child linked from the parent.

Product behavior

Persist and reuse only a validated provider-neutral route whose backend response explicitly authorizes persistent storage under the current Geoapify release policy:

  • storage uses the production SQLite/application-data ownership and survives service recreation/offline state;
  • a route matches deterministically by normalized Wayfarer server identity, provider/configuration/mapping identity, stable transport-profile identity, endpoints, and approved ordered anchors;
  • every route is partitioned by a Mobile-generated stable non-secret local server/account authority;
  • valid replacement is atomic;
  • invalid, partial, failed, cancelled, unauthorized, unavailable, or timed-out refresh never replaces or deletes the last valid retained route;
  • matching prevents reuse across another server, provider/configuration/mapping, profile, endpoint, or anchor sequence;
  • retention and eviction are deterministic and bounded;
  • connected and offline rendering preserve route source, age, and linked Geoapify/OpenStreetMap attribution;
  • Direct remains explicitly selectable and the universal fallback.

No Mapbox Directions result is authorized for persistence. Nominatim is not a routing provider.

Distinct data owners

Keep these authorities separate:

  • Trip-owned saved Segment geometry in OfflineSegmentEntity.Geometry and downloaded Trip metadata;
  • retained provider-neutral Wayfarer routes owned by this issue;
  • transient active navigation/presentation state;
  • OSM live-tile cache;
  • authentication credentials and server-side provider configuration/credentials.

Clearing or evicting retained routes must not delete or rewrite Trips, Places, Segments, saved geometry, Timeline data/cursors, queue data, preferences unrelated to routing, credentials/session state, or OSM tiles. Provider switching, key replacement, temporary outage, failed refresh, or connectivity loss alone does not delete a valid retained route.

Local account-partition authority

Use a Mobile-generated cryptographically random opaque identifier, such as a GUID, as the active local server/account partition. SettingsService is the existing protected settings/SecureStorage owner; consolidate committed server/API-credential replacement and clear/logout into one authoritative mutation path there. Onboarding and QR setup must use that path, and connection probes must not temporarily mutate committed settings or rotate authority.

  • Store the partition identifier through the existing protected settings/SecureStorage owner. Never derive it from, hash, log, export, or expose the API token, and never inspect or compare secret bytes to recover a partition.
  • Keep it stable across application restart and offline service/repository recreation while the same stored server/token authority remains active.
  • Rotate it whenever API credentials are replaced, credentials are cleared/logout occurs, configured server identity changes, or account authority is otherwise reset. Rotation occurs through the same authoritative mutation as the credential/server change so no consumer can observe a newly committed authority with the old partition.
  • Re-entering even the same token may create a new partition; isolation is preferred over recovering old retained routes.
  • Include the partition in every retained-route row, uniqueness/match key, lookup, update, successful-use recency update, and deletion scoped to the active account. Never return a row from another partition.
  • Rows from old partitions may remain only until bounded global eviction or explicit routed-guidance clearing; they are never eligible for the new partition.
  • Clearing routed guidance may remove all route partitions but must not clear credentials or unrelated data.
  • Do not persist member names, usernames, notes, tokens, or token hashes as route identity.

This is entirely local Mobile authority. Do not add a backend identity endpoint or backend child.

Canonical coordinate and request identity

#260 owns the canonicalization implementation for the immutable provider-neutral request/match identity. #261 reuses that exact owner and representation without reimplementation. Coordinates are always longitude then latitude. Require finite WGS84 longitude in [-180, 180] and latitude in [-90, 90], normalize negative zero to zero, then canonicalize each value to a signed scaled 10^-5-degree integer using value × 100000 with MidpointRounding.AwayFromZero. One integer unit equals 0.00001 degree; this is not conventional 10^-6-degree microdegree encoding. Ordinary signed 32-bit integers safely hold the validated longitude/latitude ranges after scaling.

Store and compare canonical integers, never raw binary floating-point values. Apply the same ordered canonicalization to origin, destination, and every approved anchor, preserving anchor order and structural duplicates. Exact equality of the canonical integer sequence defines endpoint/anchor identity. Do not recover structural duplicates through coordinate equality, and do not use arbitrary geographic distance, locale-sensitive formatting, raw JSON, or provider geometry as the cache key. Reject invalid/out-of-range inputs before lookup or persistence. Five-decimal canonicalization treats sub-meter representation noise as equivalent while materially changed endpoints remain distinct.

Persisted contract

Persist only bounded validated fields required to match, navigate, and explain the route:

  • normalized non-secret Wayfarer server identity;
  • local account-partition identifier;
  • stable non-secret provider, configuration, mapping, and transport-profile identities;
  • canonical integer origin, destination, and approved ordered anchors from Add authenticated provider-neutral Wayfarer routing #260's shared request identity;
  • valid ordered provider-neutral WGS84 geometry;
  • finite non-negative distance/duration when present;
  • bounded normalized instructions with valid geometry ranges;
  • local StoredAtUtc, local LastUsedAtUtc, and backend GeneratedAtUtc normalized to UTC;
  • storage mode/authority;
  • bounded attribution/source text and safe link targets;
  • only a bounded target association when a current product owner requires it.

Never store provider credentials, provider request URLs, raw provider responses, member names/notes, or provider-specific SDK objects. Reject unknown/non-persistent storage modes for durable storage. Legacy cached_osrm_route is deleted, not adopted.

Navigation authority after this issue

For saved Trip Segments:

  1. valid Trip-owned saved Segment geometry;
  2. valid matching retained Wayfarer route when applicable;
  3. fresh authenticated Wayfarer route when connected, authorized, and requested;
  4. Direct guidance.

For ad-hoc Place/member/map targets:

  1. valid matching retained Wayfarer route when applicable;
  2. fresh authenticated Wayfarer route;
  3. Direct guidance.

Direct remains explicitly selectable. Never splice or blend partial provider geometry with Direct geometry. If retained data is invalid, mismatched, or unavailable, skip it without damaging other navigation data.

Persistence, retention, migration, and cleanup

Use the smallest cohesive SQLite schema/repository/service boundary consistent with current database ownership. Retain at most 200 Wayfarer routes across the complete installation database, across all account partitions and servers. There is no automatic age expiry: offline Trips may remain useful and Geoapify rights do not require an age cutoff. Never use provider quota cycles as expiry.

On insertion/replacement, set StoredAtUtc and LastUsedAtUtc from the same trusted local receipt time. Replacement is transactional and atomic; a successful replacement cannot briefly remove the previous valid route. After a successful insert/replacement, enforce the global 200-row cap in that transaction. Evict deterministically by oldest LastUsedAtUtc, then oldest StoredAtUtc, then stable primary-key order.

On successful selection for rendering/navigation, update LastUsedAtUtc; a failed or mismatched lookup does not update recency. Eviction always uses local stored/last-used times, never the backend timestamp. Normalize backend GeneratedAtUtc to UTC. If it is more than five minutes after local receipt time, the result is not eligible for persistence, the previous valid retained route remains untouched, and transient use is allowed only when #260's validation policy explicitly deems it safe. If it is within five minutes in the future, retain it for provenance, present route age as zero until local time passes it, and continue to use local times for eviction.

Distinguish retention from eligibility: provider/configuration switching does not delete a row; mismatched authority makes it ineligible; global LRU eviction may later remove it. Schema upgrade must preserve the existing application database and settings. Cleanup is idempotent, bounded, and scoped only to retained Wayfarer routes. Explicit clear removes retained Wayfarer routes across all partitions only; it does not remove Trip data, Segment geometry, Timeline state, queues, credentials, unrelated settings, or OSM live tiles. Offline reuse performs no capability or route request.

Repository concurrency and serialization

The retained-route repository owns one narrow asynchronous serialization boundary for every operation that can mutate retained-route state: insert, replacement, cap enforcement, eviction, successful-selection recency update, and explicit route clearing. The gate protects database state only. No network contact, provider parsing, route generation, UI work, or unrelated synchronization may occur while holding it. Do not create a generic application-wide locking framework.

Save behavior is executable and ordered:

  1. Complete response validation and Add authenticated provider-neutral Wayfarer routing #260 canonicalization before entering the repository gate.
  2. Capture the Add authenticated provider-neutral Wayfarer routing #260 immutable request identity and request generation.
  3. After acquiring the repository gate and immediately before beginning the transaction, revalidate that the request remains current under Add authenticated provider-neutral Wayfarer routing #260's publication authority.
  4. If it is stale, perform no insert or replacement, eviction, or recency update; preserve the previous route and return a bounded superseded/no-write result.
  5. If it is current, perform insert or atomic replacement, timestamp assignment, global 200-row cap enforcement, and deterministic eviction in one SQLite transaction.
  6. Commit only a complete valid route.
  7. Any validation, transaction, cancellation, write, or eviction failure rolls back and preserves the prior complete route.
  8. Never delete the previous row before a replacement transaction is guaranteed to commit.

Repository operations are serialized. When two requests target the same match identity, only the request still current after acquiring the gate may replace the row. An older request that completes network work later cannot overwrite the newer navigation authority. Different route identities may be serialized by this single narrow repository gate for simplicity; per-key or distributed locking is not required. Persistence ordering must not become a second authority that overrides #260's request generation/current-state fence.

Matching lookup plus a successful LastUsedAtUtc update is coordinated under the same repository gate. At the update seam, the selected row must still satisfy account, server, provider/configuration, profile, endpoint, anchor, and current eligibility checks. Failed, mismatched, stale, or concurrently evicted lookups do not update recency. A successful offline selection updates recency exactly once. Selection may return a detached/immutable complete route only after the coordinated database operation; callers must not observe a partially updated entity.

Explicit route clearing uses the same gate. A save or recency update cannot resurrect or partially survive a clear transaction. Gate acquisition defines deterministic ordering; a later authorized operation may proceed only under the then-current account, session, and navigation authority.

Cancellation before transaction start performs no mutation. Cancellation or exception during the transaction rolls it back. Cleanup or rollback must not replace the primary failure, and the gate must not be held indefinitely after cancellation or failure.

Compatibility and authority changes

An old/provider-disabled backend remains safe: ordinary authentication, Location/Timeline synchronization and cursor continuity, Trip download/use, saved geometry, and Direct do not depend on retained routing. A retained route can be used offline only from already validated stored authority; failure to revalidate while offline does not delete it.

When a connected response provides a different stable server/provider/configuration/mapping/profile authority, it cannot match the old route. Learning that a specific authority is no longer valid prevents that authority's future selection/reuse as defined by the server contract, but a generic route failure or absence of connectivity is not revocation. Do not infer provider policy from credential presence or provider name alone; persist only the backend storage mode authorized by stef-k/Wayfarer#505.

Evidence

Use proportional TDD at the lowest stable seams. At minimum prove with production SQLite ownership:

  • validated authorized route round-trip survives repository/service recreation and offline state;
  • account A stores a route, authority rotates, and account B on the same server cannot retrieve A's route, while process/service recreation preserves B's current partition;
  • no API token or token hash enters SQLite or logs;
  • server/provider/configuration/mapping/profile/endpoint/anchor mismatch cannot reuse a route;
  • atomic replacement exposes either the prior complete route or the new complete route, never partial data;
  • failed refresh preserves the prior valid route;
  • invalid/unrecognized storage authority or malformed required fields are not persisted;
  • the 200-row global cap, deterministic 201st-row eviction, successful-use recency update, stable tie-breaker, no age-based deletion, and cross-partition counting;
  • future-clock rejection preserves the previous route, while permitted near-future provenance cannot control eviction;
  • cleanup/clear touches only retained routes and is idempotent;
  • saved Segment geometry remains first authority; retained/fresh/Direct priority works for one representative saved and ad-hoc flow;
  • attribution/source/age survive round-trip and appear with offline routed guidance;
  • no credential, raw provider response/URL, member name, note, or unrelated application data is stored.

Use a small table-driven production-SQLite selection for retention, eviction, partitioning, future-clock behavior, and cleanup isolation. Add one focused production-SQLite overlap proof through controlled seams, not wall-clock timing: begin with a valid prior route, delay older save A, make newer save B current and authoritative, then resume A and prove A cannot overwrite B. Coordinate simultaneous successful selection/recency activity and prove it cannot violate the 200-row cap, lose the complete row, or produce a partial record; inject one write or eviction failure and prove the prior complete route is preserved. Keep this to the one focused repository concurrency owner plus the existing deterministic persistence cases; do not build a generic concurrency harness or exhaustive interleaving matrix.

Do not duplicate the full validation/failure matrix already owned by #260 or repeat it through UI/device layers. Run focused pure/service/SQLite/view-model tests, then the affected suite. Compile Android Release for database/DI/presentation changes. Use exact-head GitHub Actions test as the merge gate. Final joint candidate evidence belongs to #253/#505.

Documentation and changelog

Update navigation/user guidance, architecture/services, database/data-management, API/privacy/security, troubleshooting, and shared changelog/release notes. Document storage authority, match identity, retention/eviction, offline source/age/attribution, clear behavior if exposed, failed-refresh preservation, and the distinction from Trip geometry and OSM tiles.

Exclusions

  • backend changes, provider research, Mapbox Directions, or Nominatim routing;
  • provider credentials or direct third-party contact;
  • synchronization of retained routes between devices or server-side ad-hoc-route persistence;
  • background prefetch, route packages, generic storage/routing frameworks;
  • tile-cache changes, broad database redesign, or broad Trip/Timeline/authentication/SSE refactors;
  • exhaustive provider/device/connectivity matrices;
  • deployment, publication, or release authorization.

Dependency and completion

Start only after authenticated Wayfarer routing is merged and Mobile main is synchronized. Completion requires reviewed code, production-persistence evidence, Android compilation, exact-head CI success, documentation/changelog acceptance, and synchronized main. Then #253 owns combined mobile candidate evidence; production deployment/publication remains exclusively under stef-k/Wayfarer#505.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions