Skip to content

feat: emit peer-disconnect from $sync with unfinished sync counts - #1357

Open
gmaclennan wants to merge 2 commits into
mainfrom
feat/sync-peer-disconnect-event
Open

gmaclennan wants to merge 2 commits into
mainfrom
feat/sync-peer-disconnect-event

Conversation

@gmaclennan

@gmaclennan gmaclennan commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Adds a peer-disconnect event to project.$sync, emitted when a peer connection closes. The payload is the peer's device id, isServer (true when the connection was opened by connectServers()), and the peer's syncState (initial and data, each with want and wanted) as it stood at the moment of disconnect. The state is read in #handlePeerDisconnect before disconnectPeer() clears it, which is the one place that knows both that the connection closed and how much sync with that peer was left unfinished.

The motivation is in digidem/comapeo-core-react-native#303: we want a metric for "a connection closed while sync with that peer was unfinished". That PR approximated it for servers by watching sync state from the outside. With this event, comapeo-core-react-native can forward disconnects to Sentry in a few lines, and comapeo-cloud can report the same event from its side, so we can see both ends of a disconnect and tell phone-initiated closes from network ones.

The event is emitted from inside hypercore's synchronous peer teardown, so a listener that throws is caught and logged rather than surfacing as an uncaught exception. The payload types (State, RemoteDeviceSyncState, PeerDisconnectInfo) are exported from the package root under a SyncApi namespace, so consumers don't have to derive them.

Tests: a new e2e test in test-e2e/sync.js disconnects two peers with known outstanding data and checks the reported counts, and the existing "data can be synced via a server" test now checks that a server disconnect is reported with isServer: true. The new test asserts only the counts, not isSyncEnabled, because that can flip as the other side tears down first. It also registers a throwing listener, and fails without the guard.

When a peer connection closes, SyncApi emits 'peer-disconnect' with the peer's device id, whether the connection was opened by connectServers(), and the want/wanted counts for that peer at the moment of disconnect, read before the peer's remote state is cleared.
A throwing peer-disconnect listener would propagate into hypercore's synchronous peer teardown and surface as an uncaught exception, so the emit is now wrapped and the error logged. State, RemoteDeviceSyncState and PeerDisconnectInfo are exported from the package root under a SyncApi namespace, and the stale @internal tags on the remote sync state types are removed.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant