Skip to content

feat(ws): delete an outbox row only once the server acks it - #325

Merged
DennisBauer merged 5 commits into
mainfrom
feat/ws-write-acknowledgement
Aug 1, 2026
Merged

feat(ws): delete an outbox row only once the server acks it#325
DennisBauer merged 5 commits into
mainfrom
feat/ws-write-acknowledgement

Conversation

@DennisBauer

Copy link
Copy Markdown
Contributor

No description provided.

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 DennisBauer added the feature New feature or request label Aug 1, 2026
@DennisBauer
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
DennisBauer force-pushed the feat/ws-write-acknowledgement branch from 540ef51 to 879b60b Compare August 1, 2026 12:30
@DennisBauer
DennisBauer merged commit cd3cd6d into main Aug 1, 2026
3 checks passed
@DennisBauer
DennisBauer deleted the feat/ws-write-acknowledgement branch August 1, 2026 12:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant