Skip to content
AB

Optimistic Chat UI with Idempotent Server Turns

Show messages immediately while reconciling acknowledgements, streamed text, persistence, failures, and safe retries.

Ashish BhakhandPublished

Waiting for the server before displaying a sent message makes chat feel slow. Showing it immediately creates a reconciliation problem.

Create one stable client turn ID before starting the request:

Code
let turn = PendingTurn(
    clientTurnID: UUID(),
    message: state.input,
    status: .sending
)

Append and persist the user message locally, clear the input, then begin the stream. The acknowledgement supplies the server turn identity and changes the local message to .sent.

Text chunks update one streaming assistant message rather than appending a new row for every fragment. On done, persist the final content and usage outcome.

If the connection fails, retain the original clientTurnID. A retry with the same message and ID lets the backend replay a completed result or resume its known state. Generating a new ID risks charging and executing the turn twice.

Protect against reusing an ID with different text. That is a client bug and should become a typed conflict, not another turn.

Cancellation differs from failure. Leaving a screen can cancel observation while the server may continue processing. Reloading the session should reconcile durable messages and turn status.

Test the state machine: optimistic insert, acknowledgement, fragments, completion, failure before acknowledgement, retry, replay, and cancellation. The smooth UI is only the visible half; stable identity is what makes optimism safe.

← Back to all writing