-
Notifications
You must be signed in to change notification settings - Fork 562
fix(client-presence): Announce 'attendeeConnected' for self-attendee #26123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix(client-presence): Announce 'attendeeConnected' for self-attendee #26123
Conversation
- self attendee
- is announced via 'attendeeConnected' when local client connects
- has status "Connected" when announced via 'attendeeConnected'
- is announced via 'attendeeConnected' when local client reconnects
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR fixes issue AB#56686 by ensuring the self-attendee is announced via the attendeeConnected event when the local client connects or reconnects. Previously, this event was not being emitted for the self-attendee due to a TODO comment blocking the implementation.
Key changes:
- Enabled the
attendeeConnectedevent emission for self-attendee in systemWorkspace.ts - Added
prepareDisconnectedPresencetest helper to support testing connection events from a disconnected state - Added three comprehensive test cases to verify self-attendee connection announcement behavior
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| packages/framework/presence/src/systemWorkspace.ts | Removed TODO comment and enabled event emission for self-attendee connection |
| packages/framework/presence/src/test/testUtils.ts | Added new test helper function to create presence instances in disconnected state for testing connection events |
| packages/framework/presence/src/test/presenceManager.spec.ts | Added test suite for self-attendee connection events and updated existing tests to exclude self-attendee from assertions |
| // Pass time (to mimic likely response) | ||
| clock.tick(broadcastJoinResponseDelaysMs.namedResponder + 20); | ||
|
|
||
| // Send a fake join response |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment on line 351 is missing contextual information that would help readers understand the purpose of the fake join response. The similar comment in prepareConnectedPresence (lines 227-230) includes a helpful explanation: "There are no other attendees in the session (not realistic) but this convinces the presence manager that it now has full knowledge, which enables it to respond to other's join requests accurately." Consider adding this explanation here as well for consistency and better code maintainability.
| // Send a fake join response | |
| // Send a fake join response. There are no other attendees in the session (not realistic) but | |
| // this convinces the presence manager that it now has full knowledge, which enables it to | |
| // respond to other's join requests accurately. |
jason-ha
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self-Attendee section should also have a test for attendeeDisconnected.
…nce with new helper functions
…elf not in Audience
|
jason-ha
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't get through looking at everything, but here are some notes.
Also still missing a self attendeeDisconnected case.
| processSignal: ProcessSignalFunction; | ||
| localAvgLatency: number; | ||
| } { | ||
| const localAvgLatency = 10; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't seem to be connected to anything anymore beyond return.
| // Add to quorum as a write client (consistent with buildClientDataArray creating write clients) | ||
| const quorumSize = this.quorum.getMembers().size; | ||
| this.quorum.addMember(newClientId, { | ||
| client: newMember, | ||
| sequenceNumber: 10 * quorumSize, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. I think there are a number of questions for this change:
- why is it needed / desired?
- assuming it is kept, where is the related removal?
- the timing does not appear to be correct. quorum is an op-based construct. This connect is should only go up to minimal connection status. Ops that change quorum would come later.
- There is no check to see that client is a write client and would appear in quorum.
Note that the Audience that Presence cares about is the Signal-only Audience.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll ended up adding a syncWithQuorum method to MockEphemeralRuntime instead of doing this. I think that's better for testing purposes and more just correct, but here's the responses I wrote before and my thought process.
-
When we connect/disconnect our local client in our testing, there's currently no mechanism to keep quorum in sync. This is desirable/needed for example when we test responding to ClientJoin -- since quorum is not synced with connect flow, the local client does not appear in quorum and cannot compute its join order. This only worked before because we used client2 as the designated local client and tested against it's hardcoded quorum sequenceNumber/join order. But if we had a test where we disconnect + reconnect with new clientID and try to test the same scenario it would all fall apart since quorum is not synced with the local connection flow.
-
I saw we handle removal of clients from audience and quorum in runtime.removeMember(). But in runtime.disconnect() we don't handle removal of local client from either audience or quorum, which doesn't seem right since it's impossible for local client to disconnect and still be in quorum. I think maybe calling removeMember(this.clientId) in disconnect() would make sense here(?)
-
Yeah this callout makes sense to me, the timing of audience and quorum removal at the same time is not right.
-
We build the new client doing
buildClientDataArray([clientId], 1 \* numWriteClients *\)so we always connect as write client.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that Quorum is a funny beast. When you join a document as a writer, it takes note of you being there. When you join later Quorum and traditional Audience will show add- and removeMember for everyone in the history (I think we don't see it too often because Summarization is aggressive and squashes the history.)
We largely do not require Quorum any longer. We do leverage it as fallback when other Join mechanism don't perform as intended. I hope that is very rare now. And things should be functional whether our local write client is in Quorum at time of demand or not. Ideally, we have coverage for both. What does our coverage look like for presenceDatastoreManager.ts lines 857 to 877 running just the Presence > protocol handling tests?
| this.audience.removeMember(clientId); | ||
| } | ||
|
|
||
| public syncWithQuorum(): void { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This says "sync" but it doesn't do anything with quorum members that are no longer in audience.
Mostly it needs comments.
| const quorumSize = this.quorum.getMembers().size; | ||
| this.quorum.addMember(clientId, { | ||
| client, | ||
| sequenceNumber: 10 * quorumSize, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sequence is questionable. sequenceNumber should always be going up, even if in the past quorum members were removed. At least add comments.
| // finally add new connection | ||
| // finally add new connection to both audience |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does "both audience" mean?
(I can assign it meaning, but without extra knowledge it is hard to know what it might mean.)
| * Note: This is intentionally not in the initial audience so that tests can | ||
| * properly simulate connecting a client that wasn't previously connected. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think initialLocalClientConnectionId should be in audience most of the time at Presence connection. At least I think that is the common case and it will be the exception that it is not in audience.
Can we have it in by default and pull it out for the special test cases?
| // Process remote client update signal (attendeeId-1 is then part of local client's known session). | ||
| const attendee1UpdateSendTimestamp = deltaToStart - 20; | ||
| const attendee1AvgLatency = 20; | ||
| const localAvgLatency = 10; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer not to have this hard coded. There is magic number in connect that matters. We can restore that, right?
| } | ||
| } | ||
| } | ||
| // From PresenceDatastoreManager.joinSession: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"// From PresenceDatastoreManager.joinSession" remains dropped
| clock.tick(broadcastJoinResponseDelaysMs.namedResponder + 20); | ||
| const connect = (clientConnectionId: ClientConnectionId): void => { | ||
| // This logic needs to be kept in sync with datastore manager. | ||
| // From PresenceDatastoreManager.getInteractiveMembersExcludingSelf: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pre-existing: comment is stale and should be updated (it was renamed to getAudienceInformation some time back.)
| // Simulate remote client disconnect (while local is disconnected) | ||
| runtime.audience.removeMember(knownAttendee.getConnectionId()); | ||
| runtime.connect("client8", "client2"); // Simulate local client reconnect with new connection id | ||
| runtime.connect("client8", initialLocalClientConnectionId); // Simulate local client reconnect with new connection id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels like "client8" should get similar treatment improving clarity.
Description
Fixes AB#56686 -- self-attendee is announced via 'attendeeConnected' when the local client connects.
To test this, we added
prepareDisconnectedPresencetest helper which creates a presence instance in a disconnected state. This allows us to test and setup event listeners before initial connection occurs. We then refactorprepareConnectedPresenceto use this as an intermediate step.We also refactor usage of
client2as the local client in our unit testing to be more explicit. This is so when we doprepareConnectedPresence, the client is only added to the audience during the connect step.List of test added: