iOS / Swift / Accessible systems and target evidence / Lesson 17
Offline commands and reconciliation
Model pending intent separately from confirmed business state and reconcile uncertain completion with stable operation identity.
On this page
Lesson goals
- A local queue records intent, not confirmation
- Persist stable operation identity
- Represent uncertain completion honestly
A local queue records intent, not confirmation
An offline command can preserve what a user wants to do while the service is unavailable. It does not prove the service will accept the action later. A seat may be taken, permission may expire, or the account may change before evaluation. Keep queued, sending, confirmed, rejected, and uncertain states distinguishable. Explain the pending status and cancellation limits without promising an outcome the client cannot guarantee. Connectivity status is only a hint: an active network interface does not prove the target endpoint is reachable or authorized. Let the authoritative response determine business confirmation.
Persist stable operation identity
Each queued operation needs an identity that survives retries and restart. Generating a new identity on every attempt defeats an idempotency contract because the server sees apparently distinct commands. The first fragment defines a portable command record containing operation, account, target, and quantity. The identifier here is supplied by the caller so it can be created once and then persisted. Sensitive content and credentials do not belong in this plain teaching envelope. Schema versioning, validation, and retention still need an explicit storage design before an actual queue is used.
struct QueuedReservation: Codable, Equatable, Sendable {
let operationID: String
let accountID: String
let workshopID: String
let seats: Int
}
let queued = QueuedReservation(operationID: "op-7", accountID: "lab-user",
workshopID: "w-7", seats: 2)
print(queued.operationID)
op-7
The synthetic identifier is stable input, not a production uniqueness generator. No queue or remote operation is created.
Represent uncertain completion honestly
A rejected response establishes a different fact from a lost response after sending. The latter may mean the server accepted the command but the client did not receive confirmation. The second fragment distinguishes these transitions instead of marking every transport failure rejected. Under this teaching state machine, lost confirmation leads to uncertain, which requires reconciliation by operation identity. A real service’s idempotency and query contracts decide what recovery is safe. A local enum cannot invent exactly-once delivery or force the server to forget an already committed operation.
enum Delivery: Equatable { case queued, sending, confirmed, rejected, uncertain }
enum DeliveryEvent { case begin, accepted, refused, responseLost }
func transition(_ state: Delivery, _ event: DeliveryEvent) -> Delivery {
switch (state, event) {
case (.queued, .begin): return .sending
case (.sending, .accepted): return .confirmed
case (.sending, .refused): return .rejected
case (.sending, .responseLost): return .uncertain
default: return state
}
}
print(transition(.sending, .responseLost) == .uncertain)
true
Unrecognized transitions preserve the state in this small model. A production command API should explicitly report invalid events where recovery requires it.
Define ordering and conflicts by domain meaning
Two queued commands may be independent, or one may depend on the other. A title edit followed by deletion is not equivalent to executing deletion first and retrying the edit later. Define per-record ordering, supersession, and conflict policy before introducing parallel workers. A server revision or other documented precondition can detect stale writes, while the UI must explain whether the user should retry, merge, or discard. Never prescribe last-write-wins for every domain. A conflict in a reading preference and a conflict in a reservation have different consequences and deserve different rules.
Keep account and cancellation boundaries visible
A command queued under one account must not be sent under another account merely because the app restarted or a token changed. Resolve current ownership and service authorization before delivery and decide what logout does to unsent private intent. Cancellation before send may remove local intent; cancellation after send can require a compensating command or may be impossible under the service contract. Do not erase an uncertain operation record just to make the pending badge disappear. Retain enough safe identity for reconciliation and apply a reviewed retention policy.
Test restart gaps with a synthetic transport
Use a fake store and transport to model restart after enqueue, after send but before confirmation, and after a server rejection. Verify retries reuse the same operation identity and do not cross account scopes. Exercise ordering dependencies and an unavailable record without a live service. A serialization test alone cannot prove queue durability, and a fake transport cannot prove the server’s idempotency implementation. Record those evidence limits. No persistent queue, real reservation, background worker, or device network operation was executed in this course authoring environment.
Common errors and fixes
Queued is shown as confirmed
- Why it happens
- Local intent was confused with authoritative acceptance.
- How to fix it
- Expose pending status until the service confirms.
- Prevent it next time
- Test delayed rejection after successful local enqueue.
Retries create new operation identifiers
- Why it happens
- Identity was generated per attempt instead of per command.
- How to fix it
- Persist the command identity and reuse it under the service contract.
- Prevent it next time
- Simulate a lost response followed by retry.
Unknown completion is treated as definitely rejected
- Why it happens
- Transport uncertainty erased possible server success.
- How to fix it
- Represent uncertainty and reconcile before unsafe repetition.
- Prevent it next time
- Separate rejected and response-lost fixtures.
Check your understanding
1. Why is a stable operation ID insufficient by itself to guarantee idempotency?
Show worked solution
The authoritative service must define and implement how it recognizes, scopes, retains, and responds to repeated IDs. The client preserves the identity so that contract can work. Without service support, the same string does not prevent duplicate side effects.
2. What should happen to an uncertain command when the user logs out?
Show worked solution
Preserve a safe reconciliation record under its original ownership according to policy, invalidate publication into new sessions, and do not resend under another account. Explain unresolved status where appropriate. Deleting evidence or relabelling it confirmed would conceal a real ambiguity.
Key takeaways
- A local queue records intent, not confirmation
- Test restart gaps with a synthetic transport