feat(ws): delete an outbox row only once the server acks it - #325
Merged
Conversation
messages was cold, and two createdAtStart singletons collect it, so every client held two live sockets. Broadcasts hid it — the server fans them out to all of a user's sessions and the applies are idempotent upserts — but a unicast frame lands on exactly one socket while sendMessage writes to whichever won the last-writer race in setCurrentSession.
The server now requires it and answers each write with a correlated ACK or ERROR. Minted fresh per enqueue rather than reusing the outbox row key: update rows are keyed on the entry id and upserted in place, so a second edit that reused the key would be answered from the server's replay cache with the first edit's ack and never applied. Rows queued before the column get one minted, and persisted, on first dispatch.
DennisBauer
enabled auto-merge
August 1, 2026 12:28
A write handed to a socket that dies before the server commits never happened, so treating the send as success and deleting the durable row lost it silently with nothing to retry. Sending now parks the row in flight and the correlated ACK removes it; a dropped connection releases every claim so the next drain re-sends, and the server answers a repeated requestId from its replay cache. A non-retryable ERROR rolls the optimistic write back: a create is deleted locally, a rejected edit refetches the group, and TAB_ENTRY_NOT_FOUND deletes. Uncorrelated errors are backpressure, not a verdict, and leave every row pending. cancelPendingCreate now declines a create that is already in flight — its row outlives the send, and cancelling one the server had committed would strand the entry there.
Rows survive until acked, a reconnect re-sends with the same requestId, an uncorrelated error touches nothing, and a non-retryable one rolls the write back — deleting a create, refetching the group for a rejected edit. Waits in real time rather than on the test scheduler: Room's suspend DAO calls run on the database's own dispatcher, so a drain kicked off by a connection change is not finished when the triggering call returns.
Deletes go over HTTP while creates and updates go over the socket, so the two had no shared ordering: a DELETE that overtook an unacked create got a 404, retired itself as success, and let the create commit afterwards — leaving the entry alive on the server with nothing left to remove it. A delete now waits for the ack of the write it deletes. A replayed ack carries the canonical entity even when it has since been soft-deleted, and local queries do not filter on deletedAt, so upserting one put a deleted entry back on screen. handleUpsert now removes it, matching what the paged sync does with its deletedIds. Also: nothing re-drained after a verdict, so a lost ack or a retryable error on a live socket left its row pending until an unrelated enqueue or reconnect. Acks, rejections and the ack deadline each schedule a drain, the retryable path behind a backoff. Rethrow CancellationException from the verdict handler, and parent the test scopes to backgroundScope so collectors and databases do not outlive a test.
DennisBauer
force-pushed
the
feat/ws-write-acknowledgement
branch
from
August 1, 2026 12:30
540ef51 to
879b60b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.