Repository navigation
Open
Description
#772 makes a presence wait behind a leave with the same ID that's already being transformed, so the leave is published first. Reviewing it turned up a few related gaps it doesn't cover:
- A re-created presence that gets through
receivePresencebefore its leave is still lost. It's acked but never published, and the leave then clears it on subscribers. For example, for presence without a doc,_subscribeDocVersioncalls back synchronously but_unsubscribeDocVersionwaits a tick, sodestroy()thencreate(sameId).submit()in one tick hits this. AsyncreceivePresencemiddleware that finishes out of order does too. This happens onmainas well. Simply waiting before the cache decision would break the rejected-leave rollback: a re-created presence that isn't cached yet when its leave is rejected would have the old presence restored over it. - The root cause is that
pvresets to 0 on re-creation, so subscribers treat the new presence as stale until they've applied the leave, and every async step between publisher and subscriber has to keep the order for each ID.sendPresencemiddleware runs separately for each subscriber, so an async check there can still reorder a leave and its re-creation. Apvthat never goes down for an ID would fix this at the source. The Agent could offset a re-creation'spvpast the last leave it published for that ID, or the client could carrypvacrossLocalPresenceinstances. Two more cases bear on that choice:- Even when the leave arrives first, a subscriber's
RemoteDocPresenceholds it as_pendinguntilwhenNothingPendingapplies it, at least a tick later. A re-creation arriving in that window failsmessage.pv < this._pending.pvand is dropped. - 🐛 Broadcast a leave before presence re-created with its ID #772's waiters only cover one
Agent. If a client destroys a presence and re-creates it on a new connection while the oldAgentis still transforming the leave, or before the server notices the old connection dropped and retracts it, the newAgentpublishes the re-creation with nothing to wait for. AnAgent-side offset wouldn't cover that, since the newAgentnever saw the leave; carryingpvover on the client would.
- Even when the leave arrives first, a subscriber's
create(sameId)whiledestroy()is in flight sends the leave's ack to the wrong presence.createreplaceslocalPresences[id]straight away, and the ack is routed by ID, so the old instance's destroy callback never fires, and if the new instance has reached the samepv, its callback fires instead. Either this needs a client fix, or the docs should say to wait fordestroy's callback.- Pubsub adapters must deliver one server's publishes on one channel in call order, which 🐛 Broadcast a leave before presence re-created with its ID #772 now relies on: it releases a waiting presence once the leave's
publish()has been called.MemoryPubSuband sharedb-redis-pubsub both deliver in call order, buttest/pubsub.js, which sharedb-redis-pubsub also runs in its CI, doesn't check it. Only 🐛 Broadcast a leave before presence re-created with its ID #772's waiters need this, and fixing (2) removes them, so this is only worth adding as an adapter-agnostic case there if (2) doesn't land. It shouldn't go into the adapter docs as a permanent contract.
Metadata
Metadata
Assignees
Labels
No labels
Activity
alecgibson commentedon Oct 30, 2026
One more for whoever picks up point 2: once a presence's
pvnever goes down for an ID, most of the leave handling #772 adds tolib/agent.jscan go again._leavesInFlight, and its_addLeaveInFlight,_afterLeaveHandedOverand_handOverLeaveInFlighthelpers, only exist to hold a re-created presence back until its leave has been published. With a higherpv, subscribers would just drop the stale leave if it arrived second, since remote presence already ignores anything below thepvit has seen.previousRequest.p === nullcheck in_broadcastPresenceonly exists because a re-creation'spvcan't be compared with its old incarnation's leave. A plainpvcomparison would do.It's worth deleting those in the same change, rather than leaving them to guard against something that can no longer happen.