diff --git a/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/presenceTest.spec.ts b/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/presenceTest.spec.ts index cbc477a574a1..f41a4b4ac4d0 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/presenceTest.spec.ts +++ b/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/presenceTest.spec.ts @@ -110,18 +110,6 @@ describe(`Presence with AzureClient`, () => { * Timeout for presence attendees to connect {@link AttendeeConnectedEvent} */ const allAttendeesJoinedTimeoutMs = (1000 + 200 * numClients) * timeoutMultiplier; - /** - * Timeout for workspace registration {@link WorkspaceRegisteredEvent} - */ - const workspaceRegisterTimeoutMs = 5000; - /** - * Timeout for presence update events {@link LatestMapValueUpdatedEvent} and {@link LatestValueUpdatedEvent} - */ - const stateUpdateTimeoutMs = 5000; - /** - * Timeout for {@link LatestMapValueGetResponseEvent} and {@link LatestValueGetResponseEvent} - */ - const getStateTimeoutMs = 5000; for (const writeClients of [numClients, 1]) { it(`announces 'attendeeConnected' when remote client joins session [${numClients} clients, ${writeClients} writers]`, async function () { @@ -224,266 +212,303 @@ describe(`Presence with AzureClient`, () => { await Promise.race([Promise.all(waitForDisconnected), childErrorPromise]); }); } + } + + { + /** + * Timeout for workspace registration {@link WorkspaceRegisteredEvent} + */ + const workspaceRegisterTimeoutMs = 5000; + /** + * Timeout for presence update events {@link LatestMapValueUpdatedEvent} and {@link LatestValueUpdatedEvent} + */ + const stateUpdateTimeoutMs = 5000; + /** + * Timeout for {@link LatestMapValueGetResponseEvent} and {@link LatestValueGetResponseEvent} + */ + const getStateTimeoutMs = 5000; // This test suite focuses on the synchronization of Latest state between clients. // NOTE: For testing purposes child clients will expect a Latest value of type string. - describe(`using Latest state object [${numClients} clients]`, () => { - let children: ChildProcess[]; - let childErrorPromise: Promise; - let containerCreatorAttendeeId: AttendeeId; - let attendeeIdPromises: Promise[]; - let remoteClients: ChildProcess[]; - const testValue = "testValue"; - const workspaceId = "presenceTestWorkspace"; - - beforeEach(async () => { - ({ children, childErrorPromise } = await forkChildProcesses(numClients, afterCleanUp)); - ({ containerCreatorAttendeeId, attendeeIdPromises } = await connectChildProcesses( - children, - { writeClients: numClients, readyTimeoutMs: childConnectTimeoutMs }, - )); - await Promise.all(attendeeIdPromises); - remoteClients = children.filter((_, index) => index !== 0); - // NOTE: For testing purposes child clients will expect a Latest value of type string (StateFactory.latest<{ value: string }>). - await registerWorkspaceOnChildren(children, workspaceId, { - latest: true, - timeoutMs: workspaceRegisterTimeoutMs, - }); - }); - - it(`allows clients to read Latest state from other clients [${numClients} clients]`, async () => { - // Setup - const updateEventsPromise = waitForLatestValueUpdates( - remoteClients, - workspaceId, - childErrorPromise, - stateUpdateTimeoutMs, - { fromAttendeeId: containerCreatorAttendeeId, expectedValue: testValue }, - ); - - // Act - Trigger the update - children[0].send({ - command: "setLatestValue", - workspaceId, - value: testValue, + describe(`using Latest state object`, () => { + for (const numClients of [5, 20]) { + assert(numClients > 1, "Must have at least two clients"); + /** + * Timeout for child processes to connect to container ({@link ConnectedEvent}) + */ + const childConnectTimeoutMs = 1000 * numClients * timeoutMultiplier; + + let children: ChildProcess[]; + let childErrorPromise: Promise; + let containerCreatorAttendeeId: AttendeeId; + let attendeeIdPromises: Promise[]; + let remoteClients: ChildProcess[]; + const testValue = "testValue"; + const workspaceId = "presenceTestWorkspace"; + + beforeEach(async () => { + ({ children, childErrorPromise } = await forkChildProcesses( + numClients, + afterCleanUp, + )); + ({ containerCreatorAttendeeId, attendeeIdPromises } = await connectChildProcesses( + children, + { writeClients: numClients, readyTimeoutMs: childConnectTimeoutMs }, + )); + await Promise.all(attendeeIdPromises); + remoteClients = children.filter((_, index) => index !== 0); + // NOTE: For testing purposes child clients will expect a Latest value of type string (StateFactory.latest<{ value: string }>). + await registerWorkspaceOnChildren(children, workspaceId, { + latest: true, + timeoutMs: workspaceRegisterTimeoutMs, + }); }); - const updateEvents = await updateEventsPromise; - // Verify all events are from the expected attendee - for (const updateEvent of updateEvents) { - assert.strictEqual(updateEvent.attendeeId, containerCreatorAttendeeId); - assert.deepStrictEqual(updateEvent.value, testValue); - } - - // Act - Request each remote client to read latest state from container creator - for (const child of remoteClients) { - child.send({ - command: "getLatestValue", + it(`allows clients to read Latest state from other clients [${numClients} clients]`, async () => { + // Setup + const updateEventsPromise = waitForLatestValueUpdates( + remoteClients, + workspaceId, + childErrorPromise, + stateUpdateTimeoutMs, + { fromAttendeeId: containerCreatorAttendeeId, expectedValue: testValue }, + ); + + // Act - Trigger the update + children[0].send({ + command: "setLatestValue", workspaceId, - attendeeId: containerCreatorAttendeeId, + value: testValue, }); - } - - const getResponses = await getLatestValueResponses( - remoteClients, - workspaceId, - childErrorPromise, - getStateTimeoutMs, - ); - - // Verify - all responses should contain the expected value - for (const getResponse of getResponses) { - assert.deepStrictEqual(getResponse.value, testValue); - } - }); + const updateEvents = await updateEventsPromise; + + // Verify all events are from the expected attendee + for (const updateEvent of updateEvents) { + assert.strictEqual(updateEvent.attendeeId, containerCreatorAttendeeId); + assert.deepStrictEqual(updateEvent.value, testValue); + } + + // Act - Request each remote client to read latest state from container creator + for (const child of remoteClients) { + child.send({ + command: "getLatestValue", + workspaceId, + attendeeId: containerCreatorAttendeeId, + }); + } + + const getResponses = await getLatestValueResponses( + remoteClients, + workspaceId, + childErrorPromise, + getStateTimeoutMs, + ); + + // Verify - all responses should contain the expected value + for (const getResponse of getResponses) { + assert.deepStrictEqual(getResponse.value, testValue); + } + }); + } }); // This test suite focuses on the synchronization of LatestMap state between clients. // NOTE: For testing purposes child clients will expect a LatestMap value of type Record. - describe(`using LatestMap state object [${numClients} clients]`, () => { - let children: ChildProcess[]; - let childErrorPromise: Promise; - let containerCreatorAttendeeId: AttendeeId; - let attendeeIdPromises: Promise[]; - let remoteClients: ChildProcess[]; - const workspaceId = "presenceTestWorkspace"; - const key1 = "player1"; - const key2 = "player2"; - const value1 = { name: "Alice", score: 100 }; - const value2 = { name: "Bob", score: 200 }; - - beforeEach(async () => { - ({ children, childErrorPromise } = await forkChildProcesses(numClients, afterCleanUp)); - ({ containerCreatorAttendeeId, attendeeIdPromises } = await connectChildProcesses( - children, - { writeClients: numClients, readyTimeoutMs: childConnectTimeoutMs }, - )); - await Promise.all(attendeeIdPromises); - remoteClients = children.filter((_, index) => index !== 0); - // NOTE: For testing purposes child clients will expect a LatestMap value of type Record (StateFactory.latestMap<{ value: Record }, string>). - await registerWorkspaceOnChildren(children, workspaceId, { - latestMap: true, - timeoutMs: workspaceRegisterTimeoutMs, - }); - }); - - it(`allows clients to read LatestMap values from other clients [${numClients} clients]`, async () => { - // Setup - const testKey = "cursor"; - const testValue = { x: 150, y: 300 }; - const updateEventsPromise = waitForLatestMapValueUpdates( - remoteClients, - workspaceId, - testKey, - childErrorPromise, - stateUpdateTimeoutMs, - { fromAttendeeId: containerCreatorAttendeeId, expectedValue: testValue }, - ); - - // Act - children[0].send({ - command: "setLatestMapValue", - workspaceId, - key: testKey, - value: testValue, + describe(`using LatestMap state object`, () => { + for (const numClients of [5, 20]) { + assert(numClients > 1, "Must have at least two clients"); + /** + * Timeout for child processes to connect to container ({@link ConnectedEvent}) + */ + const childConnectTimeoutMs = 1000 * numClients * timeoutMultiplier; + + let children: ChildProcess[]; + let childErrorPromise: Promise; + let containerCreatorAttendeeId: AttendeeId; + let attendeeIdPromises: Promise[]; + let remoteClients: ChildProcess[]; + const workspaceId = "presenceTestWorkspace"; + const key1 = "player1"; + const key2 = "player2"; + const value1 = { name: "Alice", score: 100 }; + const value2 = { name: "Bob", score: 200 }; + + beforeEach(async () => { + ({ children, childErrorPromise } = await forkChildProcesses( + numClients, + afterCleanUp, + )); + ({ containerCreatorAttendeeId, attendeeIdPromises } = await connectChildProcesses( + children, + { writeClients: numClients, readyTimeoutMs: childConnectTimeoutMs }, + )); + await Promise.all(attendeeIdPromises); + remoteClients = children.filter((_, index) => index !== 0); + // NOTE: For testing purposes child clients will expect a LatestMap value of type Record (StateFactory.latestMap<{ value: Record }, string>). + await registerWorkspaceOnChildren(children, workspaceId, { + latestMap: true, + timeoutMs: workspaceRegisterTimeoutMs, + }); }); - const updateEvents = await updateEventsPromise; - // Check all events are from the expected attendee - for (const updateEvent of updateEvents) { - assert.strictEqual(updateEvent.attendeeId, containerCreatorAttendeeId); - assert.strictEqual(updateEvent.key, testKey); - assert.deepStrictEqual(updateEvent.value, testValue); - } - - for (const child of remoteClients) { - child.send({ - command: "getLatestMapValue", + it(`allows clients to read LatestMap values from other clients [${numClients} clients]`, async () => { + // Setup + const testKey = "cursor"; + const testValue = { x: 150, y: 300 }; + const updateEventsPromise = waitForLatestMapValueUpdates( + remoteClients, + workspaceId, + testKey, + childErrorPromise, + stateUpdateTimeoutMs, + { fromAttendeeId: containerCreatorAttendeeId, expectedValue: testValue }, + ); + + // Act + children[0].send({ + command: "setLatestMapValue", workspaceId, key: testKey, - attendeeId: containerCreatorAttendeeId, + value: testValue, }); - } - const getResponses = await getLatestMapValueResponses( - remoteClients, - workspaceId, - testKey, - childErrorPromise, - getStateTimeoutMs, - ); - - // Verify - for (const getResponse of getResponses) { - assert.deepStrictEqual(getResponse.value, testValue); - } - }); - - it(`returns per-key values on read [${numClients} clients]`, async () => { - // Setup - const allAttendeeIds = await Promise.all(attendeeIdPromises); - const attendee0Id = containerCreatorAttendeeId; - const attendee1Id = allAttendeeIds[1]; - - const key1Recipients = children.filter((_, index) => index !== 0); - const key2Recipients = children.filter((_, index) => index !== 1); - const key1UpdateEventsPromise = waitForLatestMapValueUpdates( - key1Recipients, - workspaceId, - key1, - childErrorPromise, - stateUpdateTimeoutMs, - { fromAttendeeId: attendee0Id, expectedValue: value1 }, - ); - const key2UpdateEventsPromise = waitForLatestMapValueUpdates( - key2Recipients, - workspaceId, - key2, - childErrorPromise, - stateUpdateTimeoutMs, - { fromAttendeeId: attendee1Id, expectedValue: value2 }, - ); - - // Act - children[0].send({ - command: "setLatestMapValue", - workspaceId, - key: key1, - value: value1, - }); - const key1UpdateEvents = await key1UpdateEventsPromise; - children[1].send({ - command: "setLatestMapValue", - workspaceId, - key: key2, - value: value2, + const updateEvents = await updateEventsPromise; + + // Check all events are from the expected attendee + for (const updateEvent of updateEvents) { + assert.strictEqual(updateEvent.attendeeId, containerCreatorAttendeeId); + assert.strictEqual(updateEvent.key, testKey); + assert.deepStrictEqual(updateEvent.value, testValue); + } + + for (const child of remoteClients) { + child.send({ + command: "getLatestMapValue", + workspaceId, + key: testKey, + attendeeId: containerCreatorAttendeeId, + }); + } + const getResponses = await getLatestMapValueResponses( + remoteClients, + workspaceId, + testKey, + childErrorPromise, + getStateTimeoutMs, + ); + + // Verify + for (const getResponse of getResponses) { + assert.deepStrictEqual(getResponse.value, testValue); + } }); - const key2UpdateEvents = await key2UpdateEventsPromise; - // Verify all events are from the expected attendees - for (const updateEvent of key1UpdateEvents) { - assert.strictEqual(updateEvent.attendeeId, attendee0Id); - assert.strictEqual(updateEvent.key, key1); - assert.deepStrictEqual(updateEvent.value, value1); - } - for (const updateEvent of key2UpdateEvents) { - assert.strictEqual(updateEvent.attendeeId, attendee1Id); - assert.strictEqual(updateEvent.key, key2); - assert.deepStrictEqual(updateEvent.value, value2); - } + it(`returns per-key values on read [${numClients} clients]`, async () => { + // Setup + const allAttendeeIds = await Promise.all(attendeeIdPromises); + const attendee0Id = containerCreatorAttendeeId; + const attendee1Id = allAttendeeIds[1]; - // Read key1 of attendee0 from all children - for (const child of children) { - child.send({ - command: "getLatestMapValue", + const key1Recipients = children.filter((_, index) => index !== 0); + const key2Recipients = children.filter((_, index) => index !== 1); + const key1UpdateEventsPromise = waitForLatestMapValueUpdates( + key1Recipients, + workspaceId, + key1, + childErrorPromise, + stateUpdateTimeoutMs, + { fromAttendeeId: attendee0Id, expectedValue: value1 }, + ); + const key2UpdateEventsPromise = waitForLatestMapValueUpdates( + key2Recipients, + workspaceId, + key2, + childErrorPromise, + stateUpdateTimeoutMs, + { fromAttendeeId: attendee1Id, expectedValue: value2 }, + ); + + // Act + children[0].send({ + command: "setLatestMapValue", workspaceId, key: key1, - attendeeId: attendee0Id, + value: value1, }); - } - const key1Responses = await getLatestMapValueResponses( - children, - workspaceId, - key1, - childErrorPromise, - getStateTimeoutMs, - ); - - // Read key2 of attendee1 from all children - for (const child of children) { - child.send({ - command: "getLatestMapValue", + const key1UpdateEvents = await key1UpdateEventsPromise; + children[1].send({ + command: "setLatestMapValue", workspaceId, key: key2, - attendeeId: attendee1Id, + value: value2, }); - } - const key2Responses = await getLatestMapValueResponses( - children, - workspaceId, - key2, - childErrorPromise, - getStateTimeoutMs, - ); - - // Verify - assert.strictEqual( - key1Responses.length, - numClients, - "Expected responses from all clients for key1", - ); - assert.strictEqual( - key2Responses.length, - numClients, - "Expected responses from all clients for key2", - ); - - for (const response of key1Responses) { - assert.deepStrictEqual(response.value, value1, "Key1 value should match"); - } - for (const response of key2Responses) { - assert.deepStrictEqual(response.value, value2, "Key2 value should match"); - } - }); + const key2UpdateEvents = await key2UpdateEventsPromise; + + // Verify all events are from the expected attendees + for (const updateEvent of key1UpdateEvents) { + assert.strictEqual(updateEvent.attendeeId, attendee0Id); + assert.strictEqual(updateEvent.key, key1); + assert.deepStrictEqual(updateEvent.value, value1); + } + for (const updateEvent of key2UpdateEvents) { + assert.strictEqual(updateEvent.attendeeId, attendee1Id); + assert.strictEqual(updateEvent.key, key2); + assert.deepStrictEqual(updateEvent.value, value2); + } + + // Read key1 of attendee0 from all children + for (const child of children) { + child.send({ + command: "getLatestMapValue", + workspaceId, + key: key1, + attendeeId: attendee0Id, + }); + } + const key1Responses = await getLatestMapValueResponses( + children, + workspaceId, + key1, + childErrorPromise, + getStateTimeoutMs, + ); + + // Read key2 of attendee1 from all children + for (const child of children) { + child.send({ + command: "getLatestMapValue", + workspaceId, + key: key2, + attendeeId: attendee1Id, + }); + } + const key2Responses = await getLatestMapValueResponses( + children, + workspaceId, + key2, + childErrorPromise, + getStateTimeoutMs, + ); + + // Verify + assert.strictEqual( + key1Responses.length, + numClients, + "Expected responses from all clients for key1", + ); + assert.strictEqual( + key2Responses.length, + numClients, + "Expected responses from all clients for key2", + ); + + for (const response of key1Responses) { + assert.deepStrictEqual(response.value, value1, "Key1 value should match"); + } + for (const response of key2Responses) { + assert.deepStrictEqual(response.value, value2, "Key2 value should match"); + } + }); + } }); } });