React Native / Inclusive native systems and release evidence / Lesson 17
Offline intent and command reconciliation
Separate locally retained intent from confirmed business state and preserve stable identity across retries and account changes.
On this page
Lesson goals
- A local queue preserves intent rather than acceptance
- Keep one operation identity across attempts
- A lost response creates uncertainty, not rejection
A local queue preserves intent rather than acceptance
An offline action can record what the user wants while a service is unavailable, but cannot promise the service will accept it later. Capacity, permission, account ownership, and record revision can change before delivery. Expose queued, sending, confirmed, rejected, and uncertain states distinctly. Device connectivity does not establish endpoint reachability or successful authorization. Decide which operations are allowed offline and what the user may cancel before or after sending. AppState activation can trigger status reconciliation under a clear owner, but should not blindly repeat the most recent consequential command.
Keep one operation identity across attempts
The first program creates an immutable synthetic command envelope with a caller-supplied operation identifier. That identifier belongs to the command, not to each transport attempt. Retrying with a fresh identifier can defeat the authoritative service’s duplicate-detection or idempotency policy. Account identity is stored with the intent so restart or login cannot silently send it under another owner. The example is an in-memory value only; persistence, uniqueness generation, encryption, and server recognition are separate contracts. No actual reservation, background task, or queue store is created by this code.
function command(operationId, accountId, workshopId, seats) {
if (![operationId, accountId, workshopId].every(value => typeof value === 'string' && value !== '')
|| !Number.isSafeInteger(seats) || seats < 1) throw new Error('INVALID_COMMAND');
return Object.freeze({operationId, accountId, workshopId, seats});
}
const queued = command('op-7', 'lab-user', 'w-7', 2);
const retryPayload = {...queued};
console.log(retryPayload.operationId);
console.log(retryPayload.accountId);
console.log(Object.isFrozen(queued));
op-7
lab-user
true
Retry copies preserve the original command and account identity. Object freezing is shallow; this envelope contains primitive fields only.
A lost response creates uncertainty, not rejection
The second program models a lost response after sending as uncertain. The server might already have committed, so ordinary rejection and lost confirmation require different recovery. Reconcile by the stable operation identity through the service’s documented contract before repeating an unsafe command. A local reducer cannot guarantee exactly-once delivery or invent a server-side transaction rollback. The shown reducer preserves state for unsupported transitions; a production API may instead report an invalid event explicitly. Choose that policy deliberately and ensure the visible status reflects the authoritative facts available rather than hiding ambiguity behind a success badge.
function transition(state, event) {
if (state === 'queued' && event === 'send') return 'sending';
if (state === 'sending' && event === 'accepted') return 'confirmed';
if (state === 'sending' && event === 'refused') return 'rejected';
if (state === 'sending' && event === 'response-lost') return 'uncertain';
return state;
}
console.log(transition('sending', 'response-lost'));
console.log(transition('sending', 'accepted'));
console.log(transition('uncertain', 'accepted'));
uncertain
confirmed
uncertain
An uncertain operation is not confirmed by an unsupported transition. A real reconciliation result needs an explicitly designed event and service policy.
Ordering and cancellation depend on domain meaning
Two queued commands can be independent or causally related. Editing a title then deleting the record is not equivalent to deletion followed by a late edit. Define per-record ordering, replacement, conflict resolution, and account scope before adding parallel delivery. Last-write-wins is not universally correct for every domain. Cancellation before send can remove local intent, while cancellation after send may require a compensating command or be impossible. Keep unresolved records long enough for safe reconciliation under a reviewed retention policy. Never resend a previous account’s private command merely because a new token is available.
Test restart gaps and ownership using fakes
Simulate restart after enqueue, after send before confirmation, and after an explicit rejection. Verify retries preserve identity, a session change prevents old publication, and dependencies remain ordered. A fake store and transport can establish application decisions without devices or remote services. Serialization alone does not prove durable queue recovery, and a fake response cannot prove server idempotency. Target background scheduling, native storage, account authorization, and actual delivery require separate authorized integration evidence. The course runs only pure models and does not operate a live queue, database, scheduler, or reservation system.
Common errors and fixes
Queued intent is shown as confirmed
- Why it happens
- Local recording was confused with authoritative acceptance.
- How to fix it
- Expose pending status until confirmation or rejection is known.
- Prevent it next time
- Test a delayed server rejection after local enqueue.
Every retry receives a new operation ID
- Why it happens
- Identity was attached to attempts rather than commands.
- How to fix it
- Persist one command identity and reuse it under the service contract.
- Prevent it next time
- Simulate a lost response followed by retry.
Logout sends old intent as the next user
- Why it happens
- Queued work lacked account ownership.
- How to fix it
- Retain or discard old intent according to explicit account policy.
- Prevent it next time
- Change sessions before delivery and reconciliation.
Check your understanding
1. Does a stable operationId guarantee idempotency by itself?
Show worked solution
No. The service must define how it recognizes, scopes, retains, and answers repeated identifiers. The client preserves identity so that contract can work. Without authoritative support, reusing the same string does not prevent duplicate side effects.
2. Why should an uncertain record not be deleted merely to clear a badge?
Show worked solution
It may be the evidence needed to reconcile a command already accepted remotely. Deleting it can hide an unresolved outcome and encourage an unsafe duplicate. Preserve safe identity under the retention policy and present uncertainty honestly rather than inventing rejection or success.
Key takeaways
- A local queue preserves intent rather than acceptance
- Test restart gaps and ownership using fakes