Skip to content
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

test(client-presence): old collateral connection and stale connection tests #23351

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/framework/presence/src/test/mockEphemeralRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,21 @@ export class MockEphemeralRuntime implements IEphemeralRuntime {
this.audience.removeMember(clientId);
}

public connect(clientId: string): void {
this.clientId = clientId;
this.connected = true;
for (const listener of this.listeners.connected) {
listener(clientId);
}
}

public disconnect(): void {
this.connected = false;
for (const listener of this.listeners.disconnected) {
listener();
}
}

// #region IEphemeralRuntime

public clientId: string | undefined;
Expand Down
256 changes: 256 additions & 0 deletions packages/framework/presence/src/test/presenceManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,76 @@ describe("Presence", () => {

verifyAttendee(joinedAttendees[0], rejoinAttendeeConnectionId, attendeeSessionId);
});

it.skip("as collateral with old connection info and connected is NOT announced via `attendeeJoined`", () => {
// Setup - generate signals

// Both connection Id's unkonwn to audience
const oldAttendeeConnectionId = "client9";
const newAttendeeConnectionId = "client10";

// Rejoin signal for the collateral attendee unknown to audience
const rejoinSignal = generateBasicClientJoin(clock.now - 10, {
averageLatency: 40,
clientSessionId: "collateral-id",
clientConnectionId: newAttendeeConnectionId,
updateProviders: [initialAttendeeConnectionId],
connectionOrder: 1,
priorClientToSessionId: {
[oldAttendeeConnectionId]: {
rev: 0,
timestamp: 0,
value: "collateral-id",
},
},
});

// Response signal sent by the initial attendee responding to the collateral attendees rejoin signal
const responseSignal = generateBasicClientJoin(clock.now - 5, {
averageLatency: 20,
clientSessionId: attendeeSessionId,
clientConnectionId: initialAttendeeConnectionId,
priorClientToSessionId: {
...initialAttendeeSignal.content.data["system:presence"].clientToSessionId,
// Old connection id of rejoining attendee
// This should be ignored by local client
[oldAttendeeConnectionId]: {
rev: 0,
timestamp: 0,
value: "collateral-id",
},
},
});

// Process initial join signal so initial attendee is known
const joinedAttendees = processJoinSignals([initialAttendeeSignal]);
assert.strictEqual(
joinedAttendees.length,
1,
"Expected exactly one attendee to be announced",
);

// Simulate rejoin message from remote client
const rejoinAttendees = processJoinSignals([rejoinSignal]);
// Confirm that rejoining attendee is announced so we can verify it remains the same after response
assert.strictEqual(
rejoinAttendees.length,
1,
"Expected exactly one attendee to be announced",
);

// Act - simulate response message from remote client
const responseAttendees = processJoinSignals([responseSignal]);

// Verify - No collateral attendee should be announced by response signal and rejoined attendee information should remain unchanged
assert.strictEqual(
responseAttendees.length,
0,
"Expected no attendees to be announced",
);
// Check attendee information remains unchanged
verifyAttendee(rejoinAttendees[0], newAttendeeConnectionId, "collateral-id");
});
});

describe("that is already known", () => {
Expand Down Expand Up @@ -372,6 +442,192 @@ describe("Presence", () => {
});
}

describe("when local client disconnects", () => {
let disconnectedAttendees: ISessionClient[];
beforeEach(() => {
// Setup
assert(knownAttendee !== undefined, "No attendee was set in beforeEach");
disconnectedAttendees = [];
afterCleanUp.push(
presence.events.on("attendeeDisconnected", (attendee) => {
disconnectedAttendees.push(attendee);
}),
);
});

it.skip("updates stale attendee status to 'Disconnected' after 30s delay upon reconnection", () => {
assert(knownAttendee !== undefined, "No attendee was set in beforeEach");

// Act - disconnect & reconnect local client
runtime.disconnect(); // Simulate local client disconnect
clock.tick(1000);
runtime.connect("client6"); // Simulate local client reconnect with new connection id

// Verify - stale attendee should still be 'Connected' after 15 seconds
clock.tick(15_001);
assert.strictEqual(
knownAttendee.getConnectionStatus(),
SessionClientStatus.Connected,
"Stale attendee should still be 'Connected' after 15s",
);

// Verify - stale attendee should be 'Disconnected' after 30 seconds and announced via `attendeeDisconnected`
clock.tick(15_001);
assert.strictEqual(
knownAttendee.getConnectionStatus(),
SessionClientStatus.Disconnected,
"Stale attendee should be 'Disconnected' 30s after reconnection",
);
assert.strictEqual(
disconnectedAttendees.length,
1,
"Exactly one attendee should be announced as disconnected",
);
});

it.skip("does not update stale attendee status if local client does not reconnect", () => {
assert(knownAttendee !== undefined, "No attendee was set in beforeEach");

// Act - disconnect local client and wait 60s
runtime.disconnect();
clock.tick(60_000);

// Verify - stale attendee should still be 'Connected' after 30+ seconds
assert.strictEqual(
knownAttendee.getConnectionStatus(),
SessionClientStatus.Connected,
"Stale attendee should still be 'Connected' after 30s",
);
});

it.skip("does not update stale attendee status if local client reconnection lasts less than 30s", () => {
assert(knownAttendee !== undefined, "No attendee was set in beforeEach");

// Act - disconnect, reconnect for 15 second, then disconnect local client again
runtime.disconnect(); // First disconnect
clock.tick(1000);
runtime.connect("client6"); // Reconnect
clock.tick(15_000); // Advance 15 seconds
runtime.disconnect(); // Disconnect again
clock.tick(60_000); // Advance 60 seconds

// Verify - stale attendee should still be 'Connected' after 30+ seconds post-reconnection
assert.strictEqual(
knownAttendee.getConnectionStatus(),
SessionClientStatus.Connected,
"Stale attendee should still be 'Connected' after 30s",
);
});

it.skip("does not update attendee status to 'Disconnected' if stale attendee rejoins", () => {
assert(knownAttendee !== undefined, "No attendee was set in beforeEach");

// Act - disconnect, reconnect, then process rejoin signal from known attendee after 15s
runtime.disconnect();
clock.tick(1000);
runtime.connect("client6");
clock.tick(15_000);
const joinedAttendees = processJoinSignals([rejoinAttendeeSignal]);
clock.tick(60_000);

// Verify - rejoining attendee should still be 'Connected' with no `attendeeJoined` announced 30+ seconds post-reconnection
assert.strictEqual(
knownAttendee.getConnectionStatus(),
SessionClientStatus.Connected,
"Active attendee should still be 'Connected' 30s after reconnection",
);
assert.strictEqual(
joinedAttendees.length,
0,
"No `attendeeJoined` should be announced for rejoining attendee that's already 'Connected'",
);
});

it.skip("does not update attendee status to 'Disconnected' if stale attendee sends datastore update", () => {
assert(knownAttendee !== undefined, "No attendee was set in beforeEach");

// Act - disconnect, reconnect, process datatstore update signal from known attendee before 30s delay, then wait 60s
runtime.disconnect();
clock.tick(1000);
runtime.connect("client6");
clock.tick(15_000);
presence.processSignal(
"",
{
type: "Pres:DatastoreUpdate",
content: {
sendTimestamp: clock.now - 10,
avgLatency: 20,
data: {
"system:presence": {
clientToSessionId:
initialAttendeeSignal.content.data["system:presence"]
.clientToSessionId,
},
},
},
clientId: initialAttendeeConnectionId,
},
false,
);
clock.tick(60_000);

// Verify - active attendee should still be 'Connected' with no `attendeeDisconnected` announced 30+ seconds post-reconnection
assert.strictEqual(
knownAttendee.getConnectionStatus(),
SessionClientStatus.Connected,
"Active attendee should still be 'Connected' 30s after reconnection",
);
assert.strictEqual(
disconnectedAttendees.length,
0,
"No `attendeeDisconnected` should be announced for known attendee that's still 'Connected'",
);
});

it.skip("updates stale attendee status to 'Disconnected' only 30s after most recent reconnection", () => {
// Setup
assert(knownAttendee !== undefined, "No attendee was set in beforeEach");
assert.strictEqual(
knownAttendee.getConnectionStatus(),
SessionClientStatus.Connected,
"Known attendee is not connected",
);

// Act - disconnect & reconnect local client multiple times with 15s delay
runtime.disconnect();
clock.tick(1000);
runtime.connect("client6");

clock.tick(15_001);

runtime.disconnect();
clock.tick(1000);
runtime.connect("client7");

// Verify - stale attendee should still be connected after 15 seconds
clock.tick(15_001);
assert.strictEqual(
knownAttendee.getConnectionStatus(),
SessionClientStatus.Connected,
"Stale attendee should still be connected",
);

// Verify - stale attendee should be disconnected after 30 seconds
clock.tick(15_001);
assert.equal(
knownAttendee.getConnectionStatus(),
SessionClientStatus.Disconnected,
"Stale attendee has wrong status",
);
assert.strictEqual(
disconnectedAttendees.length,
1,
"Exactly one attendee should be announced as disconnected",
);
});
});

describe("and has their connection removed", () => {
it("is announced via `attendeeDisconnected`", () => {
// Setup
Expand Down
Loading