Ending a piano practice session sounds like one POST. The tricky case is a lost response: the server may have accepted the stop while the client still sees a timeout. Retrying with a new event ID could turn one user action into two logical events.
In Pianory's account-backed practice flow, we persist the intent before sending it. queuePracticeStop writes both the session snapshot and the pending stop into one IndexedDB readwrite transaction. The pending record contains an ordered list of tick and stop events; each event keeps its ID and seconds. If a tick request had an uncertain response, we keep that same ID for replay.
The replay path sends the events in order. It rejects a failed HTTP response, validates that the stop acknowledgement belongs to the same session and contains a nonnegative integer duration, and only then calls completePracticeStop. That final method updates the session and removes the pending record in another IndexedDB transaction.
The useful invariant is small: an uncertain network result leaves a durable pending record. A later retry refers to the same logical event, rather than inventing a fresh one. The in-memory replay guard also coalesces concurrent sync requests within a client namespace.
A focused test covers replay and retry ordering. The end-to-end check uses a mock endpoint. This does not prove live D1 behavior, exactly-once delivery, or that every elapsed second survives every possible device failure. The contract is limited to account-backed sessions; guest sessions do not queue server practice stops.
This pattern applies beyond music practice: if the user says 'done', save that intent locally before treating the network as reliable.
AI assisted with drafting this article; I checked these claims against the repository and focused tests. I make Pianory, the piano-learning app behind this example.
Top comments (0)