docs mobile p12 t03 application ports - AlexandreZanata/Abastevo GitHub Wiki
P12-T03 — Application ports and offline state
Status: LOCAL_DONE on codex/phase-12-kmp-foundation (phase PR #21, draft).
Issue: #18. Dependency P12-T02 LOCAL_DONE satisfied. No behavior change to
existing screens, no Room schema change, no KMP Gradle plugin yet.
B-BR / BUC
Contract for P10-T05 (outbox/direct media) and P10-T02 (community reads plus local cache): stable command IDs with a fresh nonce per retry, explicit queued/in-flight/failed/cancelled/acked states, stale-revision protection and honest offline/stale/empty reads. Owning integration stays in P10; this task freezes the portable state machines and the persistence shape.
What changed
Portable-ready pure Kotlin in :application
(com.anpfuel.application.portable, zero java.*/Android/Hilt imports,
guard-tested for the future commonMain move). Explicit constructor
injection only: platforms implement PortableClock/PortableNonceSource
behind the boundary; tests use inline ticks.
PortablePorts.kt—PortableClock(monotonic device ticks, no wall-clock math) andPortableNonceSource(fresh opaque value per dispatch, never reused).PortableOutbox.kt— FIFO enqueue with same-ID dedupe (revision bump, payload replaced, attempts kept so an old draft is never relabelled fresh);selectNextskips in-flight/terminal and uneligible backoff and dispatches nothing while cancelled;markInFlightattaches the fresh nonce;markAckedremoves only the exact ID+revision (stale acks ignored);markFailedapplies overflow-safe bounded exponential backoff (BASE 1000ticks, doubling, capped atMAX 3600000);cancelis terminal.toRecord/fromRecordfreeze the migration-compatible shape (unknown keys ignored, missing/corrupt keys fail loudly).PortableSyncState.kt— online remote wins asREMOTE_FRESH; otherwise cache serves asCACHE/CACHE_STALEby explicit age bound (clock skew never marks fresh data stale); absent remote+cache isEMPTY, never a silent fresh empty list.data/.../local/outbox/OutboxCommandMapper.kt— native boundary: Room adapter persists the portable record field-for-field (delegation, no translation drift). Entity/migration introduction stays a later task.- Tests:
PortableOutboxTest(8),PortableSyncStateTest(5),PortableAppParityTest(2: platform-import guard + record-shape freeze),OutboxCommandMapperTest(2) — 17 new tests.
Room remains the Android adapter; Hilt stays out of portable code; existing
coroutines/Flow gateways (NetworkConnectivityGateway, SyncExecutionLock)
are untouched boundary APIs.
Validation (exact commands, this host)
./gradlew :domain:test :application:test --no-daemon
./gradlew :data:testDebugUnitTest :app:testDebugUnitTest :app:assembleDebug --no-daemon
make quick-verify
git diff --check
Outcome:
:domain+:application: all suites pass including 17 new portable application tests, 0 failures (full counts in commit evidence).:data+:appunit tests incl.OutboxCommandMapperTestandassembleDebug: SUCCESS (no schema change; regression only).make quick-verify: ok;git diff --checkclean; secrets scan clean.
Limits (not claimed)
- No
commonMainwiring, no WorkManager/Room entity, no retry timers on device; the processor loop and DAO land in P10-T05 against this contract. - No Swift counterpart yet (fixture-shaped records are JSON-ready strings; Swift parity belongs to P12-T04/T05).
- Instrumentation and device acceptance stay deferred to P12-T05/P18.
Rollback
Pure addition behind no flag: previous behavior is bit-identical with the portable packages unused. Remove the new packages to roll back; no data migration involved (record shape is forward-only additive).
Next
P12-T04 Swift framework and native shell (#19): umbrella framework plus thin iPhone host executing a shared-fixture use case on macOS. Issue #18 stays open until the P12 phase PR merges.