@fluidframework/azure-end-to-end-tests 2.53.1 → 2.61.0-355054

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.eslintrc.cjs +27 -0
  2. package/CHANGELOG.md +4 -0
  3. package/lib/test/AzureClientFactory.js +2 -2
  4. package/lib/test/AzureClientFactory.js.map +1 -1
  5. package/lib/test/AzureTokenFactory.js +2 -2
  6. package/lib/test/AzureTokenFactory.js.map +1 -1
  7. package/lib/test/TestDataObject.js +4 -2
  8. package/lib/test/TestDataObject.js.map +1 -1
  9. package/lib/test/audience.spec.js +1 -1
  10. package/lib/test/audience.spec.js.map +1 -1
  11. package/lib/test/containerCreate.spec.js +6 -4
  12. package/lib/test/containerCreate.spec.js.map +1 -1
  13. package/lib/test/ddsTests.spec.js +1 -1
  14. package/lib/test/ddsTests.spec.js.map +1 -1
  15. package/lib/test/multiprocess/childClient.tool.js +454 -0
  16. package/lib/test/multiprocess/childClient.tool.js.map +1 -0
  17. package/lib/test/multiprocess/messageTypes.js.map +1 -1
  18. package/lib/test/multiprocess/orchestratorUtils.js +381 -0
  19. package/lib/test/multiprocess/orchestratorUtils.js.map +1 -0
  20. package/lib/test/multiprocess/presenceTest.spec.js +306 -191
  21. package/lib/test/multiprocess/presenceTest.spec.js.map +1 -1
  22. package/lib/test/tree.spec.js +3 -2
  23. package/lib/test/tree.spec.js.map +1 -1
  24. package/lib/test/utils.js.map +1 -1
  25. package/lib/test/viewContainerVersion.spec.js +1 -1
  26. package/lib/test/viewContainerVersion.spec.js.map +1 -1
  27. package/package.json +29 -29
  28. package/src/test/.mocharc.cjs +0 -1
  29. package/src/test/AzureClientFactory.ts +6 -8
  30. package/src/test/AzureTokenFactory.ts +3 -2
  31. package/src/test/TestDataObject.ts +6 -5
  32. package/src/test/audience.spec.ts +1 -1
  33. package/src/test/containerCreate.spec.ts +6 -5
  34. package/src/test/ddsTests.spec.ts +1 -1
  35. package/src/test/multiprocess/childClient.tool.ts +575 -0
  36. package/src/test/multiprocess/messageTypes.ts +138 -4
  37. package/src/test/multiprocess/orchestratorUtils.ts +573 -0
  38. package/src/test/multiprocess/presenceTest.spec.ts +454 -270
  39. package/src/test/tree.spec.ts +4 -10
  40. package/src/test/utils.ts +1 -1
  41. package/src/test/viewContainerVersion.spec.ts +1 -1
  42. package/lib/test/multiprocess/childClient.js +0 -169
  43. package/lib/test/multiprocess/childClient.js.map +0 -1
  44. package/src/test/multiprocess/childClient.ts +0 -227
@@ -3,8 +3,38 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
  import { strict as assert } from "node:assert";
6
- import { fork } from "node:child_process";
6
+ import inspector from "node:inspector";
7
7
  import { timeoutAwait, timeoutPromise } from "@fluidframework/test-utils/internal";
8
+ import { connectAndListenForAttendees, connectAndWaitForAttendees, connectChildProcesses, forkChildProcesses, getLatestMapValueResponses, getLatestValueResponses, registerWorkspaceOnChildren, testConsole, waitForLatestMapValueUpdates, waitForLatestValueUpdates, } from "./orchestratorUtils.js";
9
+ const useAzure = process.env.FLUID_CLIENT === "azure";
10
+ /**
11
+ * Detects if the debugger is attached (when code loaded).
12
+ */
13
+ const debuggerAttached = inspector.url() !== undefined;
14
+ /**
15
+ * Set this to a high number when debugging to avoid timeouts from debugging time.
16
+ */
17
+ const timeoutMultiplier = debuggerAttached ? 1000 : useAzure ? 3 : 1;
18
+ /**
19
+ * Sets the timeout for the given test context.
20
+ *
21
+ * @remarks
22
+ * If a debugger is attached, the timeout is set to 0 to prevent timeouts during debugging.
23
+ * Otherwise, it sets the timeout to the maximum of the current timeout and the specified duration.
24
+ *
25
+ * @param context - The Mocha test context.
26
+ * @param duration - The duration in milliseconds to set the timeout to. Zero disables the timeout.
27
+ */
28
+ function setTimeout(context, duration) {
29
+ const currentTimeout = context.timeout();
30
+ const newTimeout = debuggerAttached || currentTimeout === 0 || duration === 0
31
+ ? 0
32
+ : Math.max(currentTimeout, duration);
33
+ if (newTimeout !== currentTimeout) {
34
+ testConsole.log(`${context.test?.title}: setting timeout to ${newTimeout}ms (was ${currentTimeout}ms)`);
35
+ context.timeout(newTimeout);
36
+ }
37
+ }
8
38
  /**
9
39
  * This test suite is a prototype for a multi-process end to end test for Fluid using the new Presence API on AzureClient.
10
40
  * In the future we hope to expand and generalize this pattern to broadly test more Fluid features.
@@ -22,7 +52,7 @@ import { timeoutAwait, timeoutPromise } from "@fluidframework/test-utils/interna
22
52
  * - Receive response messages from child clients to verify expected behavior.
23
53
  * - Clean up child processes after each test.
24
54
  *
25
- * The child processes are located in the `childClient.ts` file. Each child process simulates a Fluid client.
55
+ * The child processes are located in the `childClient.tool.ts` file. Each child process simulates a Fluid client.
26
56
  *
27
57
  * The child client's job includes:
28
58
  * - Create/Get + connect to Fluid container.
@@ -30,165 +60,6 @@ import { timeoutAwait, timeoutPromise } from "@fluidframework/test-utils/interna
30
60
  * - Perform the requested action.
31
61
  * - Send response messages including any relevant data back to the orchestrator to verify expected behavior.
32
62
  */
33
- /**
34
- * Fork child processes to simulate multiple Fluid clients.
35
- *
36
- * @remarks
37
- * Individual child processes may be scheduled concurrently on a multi-core CPU
38
- * and separate processes will never share a port when connected to a service.
39
- *
40
- * @param numProcesses - The number of child processes to fork.
41
- * @param cleanUpAccumulator - An array to accumulate cleanup functions for
42
- * each child process. This is build per instance to accommodate any errors
43
- * that might occur before completing all forking.
44
- *
45
- * @returns A promise that resolves with an object containing the child
46
- * processes and a promise that rejects on any child process errors.
47
- */
48
- async function forkChildProcesses(numProcesses, cleanUpAccumulator) {
49
- const children = [];
50
- const childReadyPromises = [];
51
- // Collect all child process error promises into this array
52
- const childErrorPromises = [];
53
- // Fork child processes
54
- for (let i = 0; i < numProcesses; i++) {
55
- const child = fork("./lib/test/multiprocess/childClient.js", [
56
- `child${i}` /* identifier passed to child process */,
57
- ]);
58
- // Register a cleanup function to kill the child process
59
- cleanUpAccumulator.push(() => {
60
- child.kill();
61
- child.removeAllListeners();
62
- });
63
- const readyPromise = new Promise((resolve, reject) => {
64
- child.once("message", (msg) => {
65
- if (msg.event === "ack") {
66
- resolve();
67
- }
68
- else {
69
- reject(new Error(`Unexpected (non-"ack") message from child${i}: ${JSON.stringify(msg)}`));
70
- }
71
- });
72
- });
73
- childReadyPromises.push(readyPromise);
74
- const errorPromise = new Promise((_, reject) => {
75
- child.on("error", (error) => {
76
- reject(new Error(`Child${i} process errored: ${error.message}`));
77
- });
78
- });
79
- childErrorPromises.push(errorPromise);
80
- child.send({ command: "ping" });
81
- children.push(child);
82
- }
83
- // This race will be used to reject any of the following tests on any child process errors
84
- const childErrorPromise = Promise.race(childErrorPromises);
85
- // All children are always expected to connect successfully and acknowledge the ping.
86
- await Promise.race([Promise.all(childReadyPromises), childErrorPromise]);
87
- return {
88
- children,
89
- childErrorPromise,
90
- };
91
- }
92
- function composeConnectMessage(id) {
93
- return {
94
- command: "connect",
95
- user: {
96
- id: `test-user-id-${id}`,
97
- name: `test-user-name-${id}`,
98
- },
99
- };
100
- }
101
- async function connectChildProcesses(childProcesses, readyTimeoutMs) {
102
- if (childProcesses.length === 0) {
103
- throw new Error("No child processes provided for connection.");
104
- }
105
- const firstChild = childProcesses[0];
106
- const containerReadyPromise = new Promise((resolve, reject) => {
107
- firstChild.once("message", (msg) => {
108
- if (msg.event === "connected" && msg.containerId) {
109
- resolve({
110
- containerCreatorAttendeeId: msg.attendeeId,
111
- containerId: msg.containerId,
112
- });
113
- }
114
- else {
115
- reject(new Error(`Non-connected message from child0: ${JSON.stringify(msg)}`));
116
- }
117
- });
118
- });
119
- {
120
- firstChild.send(composeConnectMessage(0));
121
- }
122
- const { containerCreatorAttendeeId, containerId } = await timeoutAwait(containerReadyPromise, {
123
- durationMs: readyTimeoutMs,
124
- errorMsg: "did not receive 'connected' from child process",
125
- });
126
- const attendeeIdPromises = [];
127
- for (const [index, child] of childProcesses.entries()) {
128
- if (index === 0) {
129
- // The first child process is the container creator, it has already sent the 'connected' message.
130
- attendeeIdPromises.push(Promise.resolve(containerCreatorAttendeeId));
131
- continue;
132
- }
133
- const message = composeConnectMessage(index);
134
- // For subsequent children, send containerId but do not wait for a response.
135
- message.containerId = containerId;
136
- attendeeIdPromises.push(new Promise((resolve, reject) => {
137
- child.once("message", (msg) => {
138
- if (msg.event === "connected") {
139
- resolve(msg.attendeeId);
140
- }
141
- else if (msg.event === "error") {
142
- reject(new Error(`Child process error: ${msg.error}`));
143
- }
144
- });
145
- }));
146
- child.send(message);
147
- }
148
- if (containerCreatorAttendeeId === undefined) {
149
- throw new Error("No container creator session ID received from child processes.");
150
- }
151
- return { containerCreatorAttendeeId, attendeeIdPromises };
152
- }
153
- /**
154
- * Connects the child processes and waits for the specified number of attendees to connect.
155
- * @remarks
156
- * This function can be used directly as a test. Comments in the functionality describe the
157
- * breakdown of test blocks.
158
- *
159
- * @param children - Array of child processes to connect.
160
- * @param attendeeCountRequired - The number of attendees that must connect.
161
- * @param childConnectTimeoutMs - Timeout duration for child process connections.
162
- * @param attendeesJoinedTimeoutMs - Timeout duration for required attendees to join.
163
- * @param earlyExitPromise - Promise that resolves/rejects when the test should early exit.
164
- */
165
- async function connectAndWaitForAttendees(children, attendeeCountRequired, childConnectTimeoutMs, attendeesJoinedTimeoutMs, earlyExitPromise = Promise.resolve()) {
166
- // Setup
167
- const attendeeConnectedPromise = new Promise((resolve) => {
168
- let attendeesJoinedEvents = 0;
169
- children[0].on("message", (msg) => {
170
- if (msg.event === "attendeeConnected") {
171
- attendeesJoinedEvents++;
172
- if (attendeesJoinedEvents >= attendeeCountRequired) {
173
- resolve();
174
- }
175
- }
176
- });
177
- });
178
- // Act - connect all child processes
179
- const connectResult = await connectChildProcesses(children, childConnectTimeoutMs);
180
- Promise.all(connectResult.attendeeIdPromises)
181
- .then(() => console.log("All attendees connected."))
182
- .catch((error) => {
183
- console.error("Error connecting children:", error);
184
- });
185
- // Verify - wait for all 'attendeeConnected' events
186
- await timeoutAwait(Promise.race([attendeeConnectedPromise, earlyExitPromise]), {
187
- durationMs: attendeesJoinedTimeoutMs,
188
- errorMsg: "did not receive all 'attendeeConnected' events",
189
- });
190
- return connectResult;
191
- }
192
63
  /**
193
64
  * This particular test suite tests the following E2E functionality for Presence:
194
65
  * - Announce 'attendeeConnected' when remote client joins session.
@@ -208,38 +79,282 @@ describe(`Presence with AzureClient`, () => {
208
79
  // TODO: AB#45620: "Presence: perf: update Join pattern for scale" may help, then remove .slice.
209
80
  for (const numClients of numClientsForAttendeeTests.slice(0, 2)) {
210
81
  assert(numClients > 1, "Must have at least two clients");
211
- // Timeout duration used when waiting for response messages from child processes.
212
- const childConnectTimeoutMs = 1000 * numClients;
213
- const allConnectedTimeoutMs = 2000;
214
- it(`announces 'attendeeConnected' when remote client joins session [${numClients} clients]`, async () => {
215
- // Setup
216
- const { children, childErrorPromise } = await forkChildProcesses(numClients, afterCleanUp);
217
- // Further Setup with Act and Verify
218
- await connectAndWaitForAttendees(children, numClients - 1, childConnectTimeoutMs, allConnectedTimeoutMs, childErrorPromise);
82
+ /**
83
+ * Timeout for child processes to connect to container ({@link ConnectedEvent})
84
+ */
85
+ const childConnectTimeoutMs = 1000 * numClients * timeoutMultiplier;
86
+ /**
87
+ * Timeout for presence attendees to connect {@link AttendeeConnectedEvent}
88
+ */
89
+ const allAttendeesJoinedTimeoutMs = (1000 + 200 * numClients) * timeoutMultiplier;
90
+ for (const writeClients of [numClients, 1]) {
91
+ it(`announces 'attendeeConnected' when remote client joins session [${numClients} clients, ${writeClients} writers]`, async function () {
92
+ setTimeout(this, childConnectTimeoutMs + allAttendeesJoinedTimeoutMs + 1000);
93
+ // Setup
94
+ const { children, childErrorPromise } = await forkChildProcesses(numClients, afterCleanUp);
95
+ // Further Setup with Act and Verify
96
+ await connectAndWaitForAttendees(children, {
97
+ writeClients,
98
+ attendeeCountRequired: numClients - 1,
99
+ childConnectTimeoutMs,
100
+ allAttendeesJoinedTimeoutMs,
101
+ }, childErrorPromise);
102
+ });
103
+ it(`announces 'attendeeDisconnected' when remote client disconnects [${numClients} clients, ${writeClients} writers]`, async function () {
104
+ // TODO: AB#45620: "Presence: perf: update Join pattern for scale" can handle
105
+ // larger counts of read-only attendees. Without protocol changes tests with
106
+ // 20+ attendees exceed current limits.
107
+ if (numClients >= 20 && writeClients === 1) {
108
+ this.skip();
109
+ }
110
+ const childDisconnectTimeoutMs = 10_000 * timeoutMultiplier;
111
+ setTimeout(this, childConnectTimeoutMs +
112
+ allAttendeesJoinedTimeoutMs +
113
+ childDisconnectTimeoutMs +
114
+ 1000);
115
+ // Setup
116
+ const { children, childErrorPromise } = await forkChildProcesses(numClients, afterCleanUp);
117
+ const connectResult = await connectAndListenForAttendees(children, {
118
+ writeClients,
119
+ attendeeCountRequired: numClients - 1,
120
+ childConnectTimeoutMs,
121
+ });
122
+ // Wait for all attendees to be fully joined
123
+ // Keep a tally for debuggability
124
+ let childrenFullyJoined = 0;
125
+ const allAttendeesFullyJoined = Promise.all(
126
+ // eslint-disable-next-line @typescript-eslint/promise-function-async
127
+ connectResult.attendeeCountRequiredPromises.map((attendeeFullyJoinedPromise) => attendeeFullyJoinedPromise.then(() => childrenFullyJoined++)));
128
+ await timeoutAwait(allAttendeesFullyJoined, {
129
+ durationMs: allAttendeesJoinedTimeoutMs,
130
+ errorMsg: "Not all attendees fully joined",
131
+ }).catch((error) => {
132
+ // Ideally this information would just be in the timeout error message, but that
133
+ // must be a resolved string (not dynamic). So, just log it separately.
134
+ testConsole.log(`${childrenFullyJoined} attendees fully joined before error...`);
135
+ throw error;
136
+ });
137
+ const waitForDisconnected = children.map(async (child, index) => index === 0
138
+ ? Promise.resolve()
139
+ : timeoutPromise((resolve) => {
140
+ child.on("message", (msg) => {
141
+ if (msg.event === "attendeeDisconnected" &&
142
+ msg.attendeeId === connectResult.containerCreatorAttendeeId) {
143
+ console.log(`Child[${index}] saw creator disconnect`);
144
+ resolve();
145
+ }
146
+ });
147
+ }, {
148
+ durationMs: childDisconnectTimeoutMs,
149
+ errorMsg: `Attendee[${index}] Disconnected Timeout`,
150
+ }));
151
+ // Act - disconnect first child process
152
+ children[0].send({ command: "disconnectSelf" });
153
+ // Verify - wait for all 'attendeeDisconnected' events
154
+ await Promise.race([Promise.all(waitForDisconnected), childErrorPromise]);
155
+ });
156
+ }
157
+ }
158
+ {
159
+ /**
160
+ * Timeout for workspace registration {@link WorkspaceRegisteredEvent}
161
+ */
162
+ const workspaceRegisterTimeoutMs = 5000;
163
+ /**
164
+ * Timeout for presence update events {@link LatestMapValueUpdatedEvent} and {@link LatestValueUpdatedEvent}
165
+ */
166
+ const stateUpdateTimeoutMs = 5000;
167
+ /**
168
+ * Timeout for {@link LatestMapValueGetResponseEvent} and {@link LatestValueGetResponseEvent}
169
+ */
170
+ const getStateTimeoutMs = 5000;
171
+ // This test suite focuses on the synchronization of Latest state between clients.
172
+ // NOTE: For testing purposes child clients will expect a Latest value of type string.
173
+ describe(`using Latest state object`, () => {
174
+ for (const numClients of [5, 20]) {
175
+ assert(numClients > 1, "Must have at least two clients");
176
+ /**
177
+ * Timeout for child processes to connect to container ({@link ConnectedEvent})
178
+ */
179
+ const childConnectTimeoutMs = 1000 * numClients * timeoutMultiplier;
180
+ let children;
181
+ let childErrorPromise;
182
+ let containerCreatorAttendeeId;
183
+ let attendeeIdPromises;
184
+ let remoteClients;
185
+ const testValue = "testValue";
186
+ const workspaceId = "presenceTestWorkspace";
187
+ beforeEach(async () => {
188
+ ({ children, childErrorPromise } = await forkChildProcesses(numClients, afterCleanUp));
189
+ ({ containerCreatorAttendeeId, attendeeIdPromises } = await connectChildProcesses(children, { writeClients: numClients, readyTimeoutMs: childConnectTimeoutMs }));
190
+ await Promise.all(attendeeIdPromises);
191
+ remoteClients = children.filter((_, index) => index !== 0);
192
+ // NOTE: For testing purposes child clients will expect a Latest value of type string (StateFactory.latest<{ value: string }>).
193
+ await registerWorkspaceOnChildren(children, workspaceId, {
194
+ latest: true,
195
+ timeoutMs: workspaceRegisterTimeoutMs,
196
+ });
197
+ });
198
+ it(`allows clients to read Latest state from other clients [${numClients} clients]`, async () => {
199
+ // Setup
200
+ const updateEventsPromise = waitForLatestValueUpdates(remoteClients, workspaceId, childErrorPromise, stateUpdateTimeoutMs, { fromAttendeeId: containerCreatorAttendeeId, expectedValue: testValue });
201
+ // Act - Trigger the update
202
+ children[0].send({
203
+ command: "setLatestValue",
204
+ workspaceId,
205
+ value: testValue,
206
+ });
207
+ const updateEvents = await updateEventsPromise;
208
+ // Verify all events are from the expected attendee
209
+ for (const updateEvent of updateEvents) {
210
+ assert.strictEqual(updateEvent.attendeeId, containerCreatorAttendeeId);
211
+ assert.deepStrictEqual(updateEvent.value, testValue);
212
+ }
213
+ // Act - Request each remote client to read latest state from container creator
214
+ for (const child of remoteClients) {
215
+ child.send({
216
+ command: "getLatestValue",
217
+ workspaceId,
218
+ attendeeId: containerCreatorAttendeeId,
219
+ });
220
+ }
221
+ const getResponses = await getLatestValueResponses(remoteClients, workspaceId, childErrorPromise, getStateTimeoutMs);
222
+ // Verify - all responses should contain the expected value
223
+ for (const getResponse of getResponses) {
224
+ assert.deepStrictEqual(getResponse.value, testValue);
225
+ }
226
+ });
227
+ }
219
228
  });
220
- it(`announces 'attendeeDisconnected' when remote client disconnects [${numClients} clients]`, async () => {
221
- // Setup
222
- const { children, childErrorPromise } = await forkChildProcesses(numClients, afterCleanUp);
223
- const connectResult = await connectAndWaitForAttendees(children, numClients - 1, childConnectTimeoutMs, allConnectedTimeoutMs, childErrorPromise);
224
- const childDisconnectTimeoutMs = 10_000;
225
- const waitForDisconnected = children.map(async (child, index) => index === 0
226
- ? Promise.resolve()
227
- : timeoutPromise((resolve) => {
228
- child.on("message", (msg) => {
229
- if (msg.event === "attendeeDisconnected" &&
230
- msg.attendeeId === connectResult.containerCreatorAttendeeId) {
231
- console.log(`Child[${index}] saw creator disconnect`);
232
- resolve();
233
- }
229
+ // This test suite focuses on the synchronization of LatestMap state between clients.
230
+ // NOTE: For testing purposes child clients will expect a LatestMap value of type Record<string, string | number>.
231
+ describe(`using LatestMap state object`, () => {
232
+ for (const numClients of [5, 20]) {
233
+ assert(numClients > 1, "Must have at least two clients");
234
+ /**
235
+ * Timeout for child processes to connect to container ({@link ConnectedEvent})
236
+ */
237
+ const childConnectTimeoutMs = 1000 * numClients * timeoutMultiplier;
238
+ let children;
239
+ let childErrorPromise;
240
+ let containerCreatorAttendeeId;
241
+ let attendeeIdPromises;
242
+ let remoteClients;
243
+ const workspaceId = "presenceTestWorkspace";
244
+ const key1 = "player1";
245
+ const key2 = "player2";
246
+ const value1 = { name: "Alice", score: 100 };
247
+ const value2 = { name: "Bob", score: 200 };
248
+ beforeEach(async () => {
249
+ ({ children, childErrorPromise } = await forkChildProcesses(numClients, afterCleanUp));
250
+ ({ containerCreatorAttendeeId, attendeeIdPromises } = await connectChildProcesses(children, { writeClients: numClients, readyTimeoutMs: childConnectTimeoutMs }));
251
+ await Promise.all(attendeeIdPromises);
252
+ remoteClients = children.filter((_, index) => index !== 0);
253
+ // NOTE: For testing purposes child clients will expect a LatestMap value of type Record<string, string | number> (StateFactory.latestMap<{ value: Record<string, string | number> }, string>).
254
+ await registerWorkspaceOnChildren(children, workspaceId, {
255
+ latestMap: true,
256
+ timeoutMs: workspaceRegisterTimeoutMs,
234
257
  });
235
- }, {
236
- durationMs: childDisconnectTimeoutMs,
237
- errorMsg: `Attendee[${index}] Disconnected Timeout`,
238
- }));
239
- // Act - disconnect first child process
240
- children[0].send({ command: "disconnectSelf" });
241
- // Verify - wait for all 'attendeeDisconnected' events
242
- await Promise.race([Promise.all(waitForDisconnected), childErrorPromise]);
258
+ });
259
+ it(`allows clients to read LatestMap values from other clients [${numClients} clients]`, async () => {
260
+ // Setup
261
+ const testKey = "cursor";
262
+ const testValue = { x: 150, y: 300 };
263
+ const updateEventsPromise = waitForLatestMapValueUpdates(remoteClients, workspaceId, testKey, childErrorPromise, stateUpdateTimeoutMs, { fromAttendeeId: containerCreatorAttendeeId, expectedValue: testValue });
264
+ // Act
265
+ children[0].send({
266
+ command: "setLatestMapValue",
267
+ workspaceId,
268
+ key: testKey,
269
+ value: testValue,
270
+ });
271
+ const updateEvents = await updateEventsPromise;
272
+ // Check all events are from the expected attendee
273
+ for (const updateEvent of updateEvents) {
274
+ assert.strictEqual(updateEvent.attendeeId, containerCreatorAttendeeId);
275
+ assert.strictEqual(updateEvent.key, testKey);
276
+ assert.deepStrictEqual(updateEvent.value, testValue);
277
+ }
278
+ for (const child of remoteClients) {
279
+ child.send({
280
+ command: "getLatestMapValue",
281
+ workspaceId,
282
+ key: testKey,
283
+ attendeeId: containerCreatorAttendeeId,
284
+ });
285
+ }
286
+ const getResponses = await getLatestMapValueResponses(remoteClients, workspaceId, testKey, childErrorPromise, getStateTimeoutMs);
287
+ // Verify
288
+ for (const getResponse of getResponses) {
289
+ assert.deepStrictEqual(getResponse.value, testValue);
290
+ }
291
+ });
292
+ it(`returns per-key values on read [${numClients} clients]`, async () => {
293
+ // Setup
294
+ const allAttendeeIds = await Promise.all(attendeeIdPromises);
295
+ const attendee0Id = containerCreatorAttendeeId;
296
+ const attendee1Id = allAttendeeIds[1];
297
+ const key1Recipients = children.filter((_, index) => index !== 0);
298
+ const key2Recipients = children.filter((_, index) => index !== 1);
299
+ const key1UpdateEventsPromise = waitForLatestMapValueUpdates(key1Recipients, workspaceId, key1, childErrorPromise, stateUpdateTimeoutMs, { fromAttendeeId: attendee0Id, expectedValue: value1 });
300
+ const key2UpdateEventsPromise = waitForLatestMapValueUpdates(key2Recipients, workspaceId, key2, childErrorPromise, stateUpdateTimeoutMs, { fromAttendeeId: attendee1Id, expectedValue: value2 });
301
+ // Act
302
+ children[0].send({
303
+ command: "setLatestMapValue",
304
+ workspaceId,
305
+ key: key1,
306
+ value: value1,
307
+ });
308
+ const key1UpdateEvents = await key1UpdateEventsPromise;
309
+ children[1].send({
310
+ command: "setLatestMapValue",
311
+ workspaceId,
312
+ key: key2,
313
+ value: value2,
314
+ });
315
+ const key2UpdateEvents = await key2UpdateEventsPromise;
316
+ // Verify all events are from the expected attendees
317
+ for (const updateEvent of key1UpdateEvents) {
318
+ assert.strictEqual(updateEvent.attendeeId, attendee0Id);
319
+ assert.strictEqual(updateEvent.key, key1);
320
+ assert.deepStrictEqual(updateEvent.value, value1);
321
+ }
322
+ for (const updateEvent of key2UpdateEvents) {
323
+ assert.strictEqual(updateEvent.attendeeId, attendee1Id);
324
+ assert.strictEqual(updateEvent.key, key2);
325
+ assert.deepStrictEqual(updateEvent.value, value2);
326
+ }
327
+ // Read key1 of attendee0 from all children
328
+ for (const child of children) {
329
+ child.send({
330
+ command: "getLatestMapValue",
331
+ workspaceId,
332
+ key: key1,
333
+ attendeeId: attendee0Id,
334
+ });
335
+ }
336
+ const key1Responses = await getLatestMapValueResponses(children, workspaceId, key1, childErrorPromise, getStateTimeoutMs);
337
+ // Read key2 of attendee1 from all children
338
+ for (const child of children) {
339
+ child.send({
340
+ command: "getLatestMapValue",
341
+ workspaceId,
342
+ key: key2,
343
+ attendeeId: attendee1Id,
344
+ });
345
+ }
346
+ const key2Responses = await getLatestMapValueResponses(children, workspaceId, key2, childErrorPromise, getStateTimeoutMs);
347
+ // Verify
348
+ assert.strictEqual(key1Responses.length, numClients, "Expected responses from all clients for key1");
349
+ assert.strictEqual(key2Responses.length, numClients, "Expected responses from all clients for key2");
350
+ for (const response of key1Responses) {
351
+ assert.deepStrictEqual(response.value, value1, "Key1 value should match");
352
+ }
353
+ for (const response of key2Responses) {
354
+ assert.deepStrictEqual(response.value, value2, "Key2 value should match");
355
+ }
356
+ });
357
+ }
243
358
  });
244
359
  }
245
360
  });
@@ -1 +1 @@
1
- {"version":3,"file":"presenceTest.spec.js","sourceRoot":"","sources":["../../../src/test/multiprocess/presenceTest.spec.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { strict as assert } from \"node:assert\";\nimport { fork, type ChildProcess } from \"node:child_process\";\n\n// eslint-disable-next-line import/no-internal-modules\nimport type { AttendeeId } from \"@fluidframework/presence/beta\";\nimport { timeoutAwait, timeoutPromise } from \"@fluidframework/test-utils/internal\";\n\nimport type { ConnectCommand, MessageFromChild } from \"./messageTypes.js\";\n\n/**\n * This test suite is a prototype for a multi-process end to end test for Fluid using the new Presence API on AzureClient.\n * In the future we hope to expand and generalize this pattern to broadly test more Fluid features.\n * Other E2E tests are limited to running multiple clients on a single process which does not effectively\n * simulate real-world production scenarios where clients are usually running on different machines. Since\n * the Fluid Framework client is designed to carry most of the work burden, multi-process testing from a\n * single machine is also not representative but does at least work past some limitations of a single\n * Node.js process handling multiple clients.\n *\n * The pattern demonstrated in this test suite is as follows:\n *\n * This main test file acts as the 'Orchestrator'. The orchestrator's job includes:\n * - Fork child processes to simulate multiple Fluid clients\n * - Send command messages to child clients to perform specific Fluid actions.\n * - Receive response messages from child clients to verify expected behavior.\n * - Clean up child processes after each test.\n *\n * The child processes are located in the `childClient.ts` file. Each child process simulates a Fluid client.\n *\n * The child client's job includes:\n * - Create/Get + connect to Fluid container.\n * - Listen for command messages from the orchestrator.\n * - Perform the requested action.\n * - Send response messages including any relevant data back to the orchestrator to verify expected behavior.\n */\n\n/**\n * Fork child processes to simulate multiple Fluid clients.\n *\n * @remarks\n * Individual child processes may be scheduled concurrently on a multi-core CPU\n * and separate processes will never share a port when connected to a service.\n *\n * @param numProcesses - The number of child processes to fork.\n * @param cleanUpAccumulator - An array to accumulate cleanup functions for\n * each child process. This is build per instance to accommodate any errors\n * that might occur before completing all forking.\n *\n * @returns A promise that resolves with an object containing the child\n * processes and a promise that rejects on any child process errors.\n */\nasync function forkChildProcesses(\n\tnumProcesses: number,\n\tcleanUpAccumulator: (() => void)[],\n): Promise<{\n\tchildren: ChildProcess[];\n\t/**\n\t * Will never resolve successfully, it is only used to reject on child process error.\n\t */\n\tchildErrorPromise: Promise<void>;\n}> {\n\tconst children: ChildProcess[] = [];\n\tconst childReadyPromises: Promise<void>[] = [];\n\t// Collect all child process error promises into this array\n\tconst childErrorPromises: Promise<void>[] = [];\n\t// Fork child processes\n\tfor (let i = 0; i < numProcesses; i++) {\n\t\tconst child = fork(\"./lib/test/multiprocess/childClient.js\", [\n\t\t\t`child${i}` /* identifier passed to child process */,\n\t\t]);\n\t\t// Register a cleanup function to kill the child process\n\t\tcleanUpAccumulator.push(() => {\n\t\t\tchild.kill();\n\t\t\tchild.removeAllListeners();\n\t\t});\n\t\tconst readyPromise = new Promise<void>((resolve, reject) => {\n\t\t\tchild.once(\"message\", (msg: MessageFromChild) => {\n\t\t\t\tif (msg.event === \"ack\") {\n\t\t\t\t\tresolve();\n\t\t\t\t} else {\n\t\t\t\t\treject(\n\t\t\t\t\t\tnew Error(`Unexpected (non-\"ack\") message from child${i}: ${JSON.stringify(msg)}`),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t\tchildReadyPromises.push(readyPromise);\n\t\tconst errorPromise = new Promise<void>((_, reject) => {\n\t\t\tchild.on(\"error\", (error) => {\n\t\t\t\treject(new Error(`Child${i} process errored: ${error.message}`));\n\t\t\t});\n\t\t});\n\t\tchildErrorPromises.push(errorPromise);\n\n\t\tchild.send({ command: \"ping\" });\n\n\t\tchildren.push(child);\n\t}\n\t// This race will be used to reject any of the following tests on any child process errors\n\tconst childErrorPromise = Promise.race(childErrorPromises);\n\n\t// All children are always expected to connect successfully and acknowledge the ping.\n\tawait Promise.race([Promise.all(childReadyPromises), childErrorPromise]);\n\n\treturn {\n\t\tchildren,\n\t\tchildErrorPromise,\n\t};\n}\n\nfunction composeConnectMessage(id: string | number): ConnectCommand {\n\treturn {\n\t\tcommand: \"connect\",\n\t\tuser: {\n\t\t\tid: `test-user-id-${id}`,\n\t\t\tname: `test-user-name-${id}`,\n\t\t},\n\t};\n}\n\nasync function connectChildProcesses(\n\tchildProcesses: ChildProcess[],\n\treadyTimeoutMs: number,\n): Promise<{\n\tcontainerCreatorAttendeeId: AttendeeId;\n\tattendeeIdPromises: Promise<AttendeeId>[];\n}> {\n\tif (childProcesses.length === 0) {\n\t\tthrow new Error(\"No child processes provided for connection.\");\n\t}\n\tconst firstChild = childProcesses[0];\n\tconst containerReadyPromise = new Promise<{\n\t\tcontainerCreatorAttendeeId: AttendeeId;\n\t\tcontainerId: string;\n\t}>((resolve, reject) => {\n\t\tfirstChild.once(\"message\", (msg: MessageFromChild) => {\n\t\t\tif (msg.event === \"connected\" && msg.containerId) {\n\t\t\t\tresolve({\n\t\t\t\t\tcontainerCreatorAttendeeId: msg.attendeeId,\n\t\t\t\t\tcontainerId: msg.containerId,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\treject(new Error(`Non-connected message from child0: ${JSON.stringify(msg)}`));\n\t\t\t}\n\t\t});\n\t});\n\t{\n\t\tfirstChild.send(composeConnectMessage(0));\n\t}\n\tconst { containerCreatorAttendeeId, containerId } = await timeoutAwait(\n\t\tcontainerReadyPromise,\n\t\t{\n\t\t\tdurationMs: readyTimeoutMs,\n\t\t\terrorMsg: \"did not receive 'connected' from child process\",\n\t\t},\n\t);\n\n\tconst attendeeIdPromises: Promise<AttendeeId>[] = [];\n\tfor (const [index, child] of childProcesses.entries()) {\n\t\tif (index === 0) {\n\t\t\t// The first child process is the container creator, it has already sent the 'connected' message.\n\t\t\tattendeeIdPromises.push(Promise.resolve(containerCreatorAttendeeId));\n\t\t\tcontinue;\n\t\t}\n\t\tconst message = composeConnectMessage(index);\n\n\t\t// For subsequent children, send containerId but do not wait for a response.\n\t\tmessage.containerId = containerId;\n\n\t\tattendeeIdPromises.push(\n\t\t\tnew Promise<AttendeeId>((resolve, reject) => {\n\t\t\t\tchild.once(\"message\", (msg: MessageFromChild) => {\n\t\t\t\t\tif (msg.event === \"connected\") {\n\t\t\t\t\t\tresolve(msg.attendeeId);\n\t\t\t\t\t} else if (msg.event === \"error\") {\n\t\t\t\t\t\treject(new Error(`Child process error: ${msg.error}`));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}),\n\t\t);\n\n\t\tchild.send(message);\n\t}\n\n\tif (containerCreatorAttendeeId === undefined) {\n\t\tthrow new Error(\"No container creator session ID received from child processes.\");\n\t}\n\n\treturn { containerCreatorAttendeeId, attendeeIdPromises };\n}\n\n/**\n * Connects the child processes and waits for the specified number of attendees to connect.\n * @remarks\n * This function can be used directly as a test. Comments in the functionality describe the\n * breakdown of test blocks.\n *\n * @param children - Array of child processes to connect.\n * @param attendeeCountRequired - The number of attendees that must connect.\n * @param childConnectTimeoutMs - Timeout duration for child process connections.\n * @param attendeesJoinedTimeoutMs - Timeout duration for required attendees to join.\n * @param earlyExitPromise - Promise that resolves/rejects when the test should early exit.\n */\nasync function connectAndWaitForAttendees(\n\tchildren: ChildProcess[],\n\tattendeeCountRequired: number,\n\tchildConnectTimeoutMs: number,\n\tattendeesJoinedTimeoutMs: number,\n\tearlyExitPromise: Promise<void> = Promise.resolve(),\n): Promise<{ containerCreatorAttendeeId: AttendeeId }> {\n\t// Setup\n\tconst attendeeConnectedPromise = new Promise<void>((resolve) => {\n\t\tlet attendeesJoinedEvents = 0;\n\t\tchildren[0].on(\"message\", (msg: MessageFromChild) => {\n\t\t\tif (msg.event === \"attendeeConnected\") {\n\t\t\t\tattendeesJoinedEvents++;\n\t\t\t\tif (attendeesJoinedEvents >= attendeeCountRequired) {\n\t\t\t\t\tresolve();\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t});\n\n\t// Act - connect all child processes\n\tconst connectResult = await connectChildProcesses(children, childConnectTimeoutMs);\n\n\tPromise.all(connectResult.attendeeIdPromises)\n\t\t.then(() => console.log(\"All attendees connected.\"))\n\t\t.catch((error) => {\n\t\t\tconsole.error(\"Error connecting children:\", error);\n\t\t});\n\n\t// Verify - wait for all 'attendeeConnected' events\n\tawait timeoutAwait(Promise.race([attendeeConnectedPromise, earlyExitPromise]), {\n\t\tdurationMs: attendeesJoinedTimeoutMs,\n\t\terrorMsg: \"did not receive all 'attendeeConnected' events\",\n\t});\n\n\treturn connectResult;\n}\n\n/**\n * This particular test suite tests the following E2E functionality for Presence:\n * - Announce 'attendeeConnected' when remote client joins session.\n * - Announce 'attendeeDisconnected' when remote client disconnects.\n */\ndescribe(`Presence with AzureClient`, () => {\n\tconst afterCleanUp: (() => void)[] = [];\n\n\t// After each test, call any cleanup functions that were registered (kill each child process)\n\tafterEach(async () => {\n\t\tfor (const cleanUp of afterCleanUp) {\n\t\t\tcleanUp();\n\t\t}\n\t\tafterCleanUp.length = 0;\n\t});\n\n\t// Note that on slower systems 50+ clients may take too long to join.\n\tconst numClientsForAttendeeTests = [5, 20, 50, 100];\n\t// TODO: AB#45620: \"Presence: perf: update Join pattern for scale\" may help, then remove .slice.\n\tfor (const numClients of numClientsForAttendeeTests.slice(0, 2)) {\n\t\tassert(numClients > 1, \"Must have at least two clients\");\n\n\t\t// Timeout duration used when waiting for response messages from child processes.\n\t\tconst childConnectTimeoutMs = 1000 * numClients;\n\t\tconst allConnectedTimeoutMs = 2000;\n\n\t\tit(`announces 'attendeeConnected' when remote client joins session [${numClients} clients]`, async () => {\n\t\t\t// Setup\n\t\t\tconst { children, childErrorPromise } = await forkChildProcesses(\n\t\t\t\tnumClients,\n\t\t\t\tafterCleanUp,\n\t\t\t);\n\n\t\t\t// Further Setup with Act and Verify\n\t\t\tawait connectAndWaitForAttendees(\n\t\t\t\tchildren,\n\t\t\t\tnumClients - 1,\n\t\t\t\tchildConnectTimeoutMs,\n\t\t\t\tallConnectedTimeoutMs,\n\t\t\t\tchildErrorPromise,\n\t\t\t);\n\t\t});\n\n\t\tit(`announces 'attendeeDisconnected' when remote client disconnects [${numClients} clients]`, async () => {\n\t\t\t// Setup\n\t\t\tconst { children, childErrorPromise } = await forkChildProcesses(\n\t\t\t\tnumClients,\n\t\t\t\tafterCleanUp,\n\t\t\t);\n\n\t\t\tconst connectResult = await connectAndWaitForAttendees(\n\t\t\t\tchildren,\n\t\t\t\tnumClients - 1,\n\t\t\t\tchildConnectTimeoutMs,\n\t\t\t\tallConnectedTimeoutMs,\n\t\t\t\tchildErrorPromise,\n\t\t\t);\n\n\t\t\tconst childDisconnectTimeoutMs = 10_000;\n\n\t\t\tconst waitForDisconnected = children.map(async (child, index) =>\n\t\t\t\tindex === 0\n\t\t\t\t\t? Promise.resolve()\n\t\t\t\t\t: timeoutPromise(\n\t\t\t\t\t\t\t(resolve) => {\n\t\t\t\t\t\t\t\tchild.on(\"message\", (msg: MessageFromChild) => {\n\t\t\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t\t\tmsg.event === \"attendeeDisconnected\" &&\n\t\t\t\t\t\t\t\t\t\tmsg.attendeeId === connectResult.containerCreatorAttendeeId\n\t\t\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\t\t\tconsole.log(`Child[${index}] saw creator disconnect`);\n\t\t\t\t\t\t\t\t\t\tresolve();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tdurationMs: childDisconnectTimeoutMs,\n\t\t\t\t\t\t\t\terrorMsg: `Attendee[${index}] Disconnected Timeout`,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t),\n\t\t\t);\n\n\t\t\t// Act - disconnect first child process\n\t\t\tchildren[0].send({ command: \"disconnectSelf\" });\n\n\t\t\t// Verify - wait for all 'attendeeDisconnected' events\n\t\t\tawait Promise.race([Promise.all(waitForDisconnected), childErrorPromise]);\n\t\t});\n\t}\n});\n"]}
1
+ {"version":3,"file":"presenceTest.spec.js","sourceRoot":"","sources":["../../../src/test/multiprocess/presenceTest.spec.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { strict as assert } from \"node:assert\";\nimport type { ChildProcess } from \"node:child_process\";\nimport inspector from \"node:inspector\";\n\nimport type { AttendeeId } from \"@fluidframework/presence/beta\";\nimport { timeoutAwait, timeoutPromise } from \"@fluidframework/test-utils/internal\";\n\nimport type { MessageFromChild } from \"./messageTypes.js\";\nimport {\n\tconnectAndListenForAttendees,\n\tconnectAndWaitForAttendees,\n\tconnectChildProcesses,\n\tforkChildProcesses,\n\tgetLatestMapValueResponses,\n\tgetLatestValueResponses,\n\tregisterWorkspaceOnChildren,\n\ttestConsole,\n\twaitForLatestMapValueUpdates,\n\twaitForLatestValueUpdates,\n} from \"./orchestratorUtils.js\";\n\nconst useAzure = process.env.FLUID_CLIENT === \"azure\";\n\n/**\n * Detects if the debugger is attached (when code loaded).\n */\nconst debuggerAttached = inspector.url() !== undefined;\n\n/**\n * Set this to a high number when debugging to avoid timeouts from debugging time.\n */\nconst timeoutMultiplier = debuggerAttached ? 1000 : useAzure ? 3 : 1;\n\n/**\n * Sets the timeout for the given test context.\n *\n * @remarks\n * If a debugger is attached, the timeout is set to 0 to prevent timeouts during debugging.\n * Otherwise, it sets the timeout to the maximum of the current timeout and the specified duration.\n *\n * @param context - The Mocha test context.\n * @param duration - The duration in milliseconds to set the timeout to. Zero disables the timeout.\n */\nfunction setTimeout(context: Mocha.Context, duration: number): void {\n\tconst currentTimeout = context.timeout();\n\tconst newTimeout =\n\t\tdebuggerAttached || currentTimeout === 0 || duration === 0\n\t\t\t? 0\n\t\t\t: Math.max(currentTimeout, duration);\n\tif (newTimeout !== currentTimeout) {\n\t\ttestConsole.log(\n\t\t\t`${context.test?.title}: setting timeout to ${newTimeout}ms (was ${currentTimeout}ms)`,\n\t\t);\n\t\tcontext.timeout(newTimeout);\n\t}\n}\n\n/**\n * This test suite is a prototype for a multi-process end to end test for Fluid using the new Presence API on AzureClient.\n * In the future we hope to expand and generalize this pattern to broadly test more Fluid features.\n * Other E2E tests are limited to running multiple clients on a single process which does not effectively\n * simulate real-world production scenarios where clients are usually running on different machines. Since\n * the Fluid Framework client is designed to carry most of the work burden, multi-process testing from a\n * single machine is also not representative but does at least work past some limitations of a single\n * Node.js process handling multiple clients.\n *\n * The pattern demonstrated in this test suite is as follows:\n *\n * This main test file acts as the 'Orchestrator'. The orchestrator's job includes:\n * - Fork child processes to simulate multiple Fluid clients\n * - Send command messages to child clients to perform specific Fluid actions.\n * - Receive response messages from child clients to verify expected behavior.\n * - Clean up child processes after each test.\n *\n * The child processes are located in the `childClient.tool.ts` file. Each child process simulates a Fluid client.\n *\n * The child client's job includes:\n * - Create/Get + connect to Fluid container.\n * - Listen for command messages from the orchestrator.\n * - Perform the requested action.\n * - Send response messages including any relevant data back to the orchestrator to verify expected behavior.\n */\n\n/**\n * This particular test suite tests the following E2E functionality for Presence:\n * - Announce 'attendeeConnected' when remote client joins session.\n * - Announce 'attendeeDisconnected' when remote client disconnects.\n */\ndescribe(`Presence with AzureClient`, () => {\n\tconst afterCleanUp: (() => void)[] = [];\n\n\t// After each test, call any cleanup functions that were registered (kill each child process)\n\tafterEach(async () => {\n\t\tfor (const cleanUp of afterCleanUp) {\n\t\t\tcleanUp();\n\t\t}\n\t\tafterCleanUp.length = 0;\n\t});\n\n\t// Note that on slower systems 50+ clients may take too long to join.\n\tconst numClientsForAttendeeTests = [5, 20, 50, 100];\n\t// TODO: AB#45620: \"Presence: perf: update Join pattern for scale\" may help, then remove .slice.\n\tfor (const numClients of numClientsForAttendeeTests.slice(0, 2)) {\n\t\tassert(numClients > 1, \"Must have at least two clients\");\n\t\t/**\n\t\t * Timeout for child processes to connect to container ({@link ConnectedEvent})\n\t\t */\n\t\tconst childConnectTimeoutMs = 1000 * numClients * timeoutMultiplier;\n\t\t/**\n\t\t * Timeout for presence attendees to connect {@link AttendeeConnectedEvent}\n\t\t */\n\t\tconst allAttendeesJoinedTimeoutMs = (1000 + 200 * numClients) * timeoutMultiplier;\n\n\t\tfor (const writeClients of [numClients, 1]) {\n\t\t\tit(`announces 'attendeeConnected' when remote client joins session [${numClients} clients, ${writeClients} writers]`, async function () {\n\t\t\t\tsetTimeout(this, childConnectTimeoutMs + allAttendeesJoinedTimeoutMs + 1000);\n\n\t\t\t\t// Setup\n\t\t\t\tconst { children, childErrorPromise } = await forkChildProcesses(\n\t\t\t\t\tnumClients,\n\t\t\t\t\tafterCleanUp,\n\t\t\t\t);\n\n\t\t\t\t// Further Setup with Act and Verify\n\t\t\t\tawait connectAndWaitForAttendees(\n\t\t\t\t\tchildren,\n\t\t\t\t\t{\n\t\t\t\t\t\twriteClients,\n\t\t\t\t\t\tattendeeCountRequired: numClients - 1,\n\t\t\t\t\t\tchildConnectTimeoutMs,\n\t\t\t\t\t\tallAttendeesJoinedTimeoutMs,\n\t\t\t\t\t},\n\t\t\t\t\tchildErrorPromise,\n\t\t\t\t);\n\t\t\t});\n\n\t\t\tit(`announces 'attendeeDisconnected' when remote client disconnects [${numClients} clients, ${writeClients} writers]`, async function () {\n\t\t\t\t// TODO: AB#45620: \"Presence: perf: update Join pattern for scale\" can handle\n\t\t\t\t// larger counts of read-only attendees. Without protocol changes tests with\n\t\t\t\t// 20+ attendees exceed current limits.\n\t\t\t\tif (numClients >= 20 && writeClients === 1) {\n\t\t\t\t\tthis.skip();\n\t\t\t\t}\n\n\t\t\t\tconst childDisconnectTimeoutMs = 10_000 * timeoutMultiplier;\n\n\t\t\t\tsetTimeout(\n\t\t\t\t\tthis,\n\t\t\t\t\tchildConnectTimeoutMs +\n\t\t\t\t\t\tallAttendeesJoinedTimeoutMs +\n\t\t\t\t\t\tchildDisconnectTimeoutMs +\n\t\t\t\t\t\t1000,\n\t\t\t\t);\n\n\t\t\t\t// Setup\n\t\t\t\tconst { children, childErrorPromise } = await forkChildProcesses(\n\t\t\t\t\tnumClients,\n\t\t\t\t\tafterCleanUp,\n\t\t\t\t);\n\n\t\t\t\tconst connectResult = await connectAndListenForAttendees(children, {\n\t\t\t\t\twriteClients,\n\t\t\t\t\tattendeeCountRequired: numClients - 1,\n\t\t\t\t\tchildConnectTimeoutMs,\n\t\t\t\t});\n\n\t\t\t\t// Wait for all attendees to be fully joined\n\t\t\t\t// Keep a tally for debuggability\n\t\t\t\tlet childrenFullyJoined = 0;\n\t\t\t\tconst allAttendeesFullyJoined = Promise.all(\n\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/promise-function-async\n\t\t\t\t\tconnectResult.attendeeCountRequiredPromises.map((attendeeFullyJoinedPromise) =>\n\t\t\t\t\t\tattendeeFullyJoinedPromise.then(() => childrenFullyJoined++),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tawait timeoutAwait(allAttendeesFullyJoined, {\n\t\t\t\t\tdurationMs: allAttendeesJoinedTimeoutMs,\n\t\t\t\t\terrorMsg: \"Not all attendees fully joined\",\n\t\t\t\t}).catch((error) => {\n\t\t\t\t\t// Ideally this information would just be in the timeout error message, but that\n\t\t\t\t\t// must be a resolved string (not dynamic). So, just log it separately.\n\t\t\t\t\ttestConsole.log(`${childrenFullyJoined} attendees fully joined before error...`);\n\t\t\t\t\tthrow error;\n\t\t\t\t});\n\n\t\t\t\tconst waitForDisconnected = children.map(async (child, index) =>\n\t\t\t\t\tindex === 0\n\t\t\t\t\t\t? Promise.resolve()\n\t\t\t\t\t\t: timeoutPromise(\n\t\t\t\t\t\t\t\t(resolve) => {\n\t\t\t\t\t\t\t\t\tchild.on(\"message\", (msg: MessageFromChild) => {\n\t\t\t\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t\t\t\tmsg.event === \"attendeeDisconnected\" &&\n\t\t\t\t\t\t\t\t\t\t\tmsg.attendeeId === connectResult.containerCreatorAttendeeId\n\t\t\t\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\t\t\t\tconsole.log(`Child[${index}] saw creator disconnect`);\n\t\t\t\t\t\t\t\t\t\t\tresolve();\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tdurationMs: childDisconnectTimeoutMs,\n\t\t\t\t\t\t\t\t\terrorMsg: `Attendee[${index}] Disconnected Timeout`,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t),\n\t\t\t\t);\n\n\t\t\t\t// Act - disconnect first child process\n\t\t\t\tchildren[0].send({ command: \"disconnectSelf\" });\n\n\t\t\t\t// Verify - wait for all 'attendeeDisconnected' events\n\t\t\t\tawait Promise.race([Promise.all(waitForDisconnected), childErrorPromise]);\n\t\t\t});\n\t\t}\n\t}\n\n\t{\n\t\t/**\n\t\t * Timeout for workspace registration {@link WorkspaceRegisteredEvent}\n\t\t */\n\t\tconst workspaceRegisterTimeoutMs = 5000;\n\t\t/**\n\t\t * Timeout for presence update events {@link LatestMapValueUpdatedEvent} and {@link LatestValueUpdatedEvent}\n\t\t */\n\t\tconst stateUpdateTimeoutMs = 5000;\n\t\t/**\n\t\t * Timeout for {@link LatestMapValueGetResponseEvent} and {@link LatestValueGetResponseEvent}\n\t\t */\n\t\tconst getStateTimeoutMs = 5000;\n\n\t\t// This test suite focuses on the synchronization of Latest state between clients.\n\t\t// NOTE: For testing purposes child clients will expect a Latest value of type string.\n\t\tdescribe(`using Latest state object`, () => {\n\t\t\tfor (const numClients of [5, 20]) {\n\t\t\t\tassert(numClients > 1, \"Must have at least two clients\");\n\t\t\t\t/**\n\t\t\t\t * Timeout for child processes to connect to container ({@link ConnectedEvent})\n\t\t\t\t */\n\t\t\t\tconst childConnectTimeoutMs = 1000 * numClients * timeoutMultiplier;\n\n\t\t\t\tlet children: ChildProcess[];\n\t\t\t\tlet childErrorPromise: Promise<never>;\n\t\t\t\tlet containerCreatorAttendeeId: AttendeeId;\n\t\t\t\tlet attendeeIdPromises: Promise<AttendeeId>[];\n\t\t\t\tlet remoteClients: ChildProcess[];\n\t\t\t\tconst testValue = \"testValue\";\n\t\t\t\tconst workspaceId = \"presenceTestWorkspace\";\n\n\t\t\t\tbeforeEach(async () => {\n\t\t\t\t\t({ children, childErrorPromise } = await forkChildProcesses(\n\t\t\t\t\t\tnumClients,\n\t\t\t\t\t\tafterCleanUp,\n\t\t\t\t\t));\n\t\t\t\t\t({ containerCreatorAttendeeId, attendeeIdPromises } = await connectChildProcesses(\n\t\t\t\t\t\tchildren,\n\t\t\t\t\t\t{ writeClients: numClients, readyTimeoutMs: childConnectTimeoutMs },\n\t\t\t\t\t));\n\t\t\t\t\tawait Promise.all(attendeeIdPromises);\n\t\t\t\t\tremoteClients = children.filter((_, index) => index !== 0);\n\t\t\t\t\t// NOTE: For testing purposes child clients will expect a Latest value of type string (StateFactory.latest<{ value: string }>).\n\t\t\t\t\tawait registerWorkspaceOnChildren(children, workspaceId, {\n\t\t\t\t\t\tlatest: true,\n\t\t\t\t\t\ttimeoutMs: workspaceRegisterTimeoutMs,\n\t\t\t\t\t});\n\t\t\t\t});\n\n\t\t\t\tit(`allows clients to read Latest state from other clients [${numClients} clients]`, async () => {\n\t\t\t\t\t// Setup\n\t\t\t\t\tconst updateEventsPromise = waitForLatestValueUpdates(\n\t\t\t\t\t\tremoteClients,\n\t\t\t\t\t\tworkspaceId,\n\t\t\t\t\t\tchildErrorPromise,\n\t\t\t\t\t\tstateUpdateTimeoutMs,\n\t\t\t\t\t\t{ fromAttendeeId: containerCreatorAttendeeId, expectedValue: testValue },\n\t\t\t\t\t);\n\n\t\t\t\t\t// Act - Trigger the update\n\t\t\t\t\tchildren[0].send({\n\t\t\t\t\t\tcommand: \"setLatestValue\",\n\t\t\t\t\t\tworkspaceId,\n\t\t\t\t\t\tvalue: testValue,\n\t\t\t\t\t});\n\t\t\t\t\tconst updateEvents = await updateEventsPromise;\n\n\t\t\t\t\t// Verify all events are from the expected attendee\n\t\t\t\t\tfor (const updateEvent of updateEvents) {\n\t\t\t\t\t\tassert.strictEqual(updateEvent.attendeeId, containerCreatorAttendeeId);\n\t\t\t\t\t\tassert.deepStrictEqual(updateEvent.value, testValue);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Act - Request each remote client to read latest state from container creator\n\t\t\t\t\tfor (const child of remoteClients) {\n\t\t\t\t\t\tchild.send({\n\t\t\t\t\t\t\tcommand: \"getLatestValue\",\n\t\t\t\t\t\t\tworkspaceId,\n\t\t\t\t\t\t\tattendeeId: containerCreatorAttendeeId,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tconst getResponses = await getLatestValueResponses(\n\t\t\t\t\t\tremoteClients,\n\t\t\t\t\t\tworkspaceId,\n\t\t\t\t\t\tchildErrorPromise,\n\t\t\t\t\t\tgetStateTimeoutMs,\n\t\t\t\t\t);\n\n\t\t\t\t\t// Verify - all responses should contain the expected value\n\t\t\t\t\tfor (const getResponse of getResponses) {\n\t\t\t\t\t\tassert.deepStrictEqual(getResponse.value, testValue);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\n\t\t// This test suite focuses on the synchronization of LatestMap state between clients.\n\t\t// NOTE: For testing purposes child clients will expect a LatestMap value of type Record<string, string | number>.\n\t\tdescribe(`using LatestMap state object`, () => {\n\t\t\tfor (const numClients of [5, 20]) {\n\t\t\t\tassert(numClients > 1, \"Must have at least two clients\");\n\t\t\t\t/**\n\t\t\t\t * Timeout for child processes to connect to container ({@link ConnectedEvent})\n\t\t\t\t */\n\t\t\t\tconst childConnectTimeoutMs = 1000 * numClients * timeoutMultiplier;\n\n\t\t\t\tlet children: ChildProcess[];\n\t\t\t\tlet childErrorPromise: Promise<never>;\n\t\t\t\tlet containerCreatorAttendeeId: AttendeeId;\n\t\t\t\tlet attendeeIdPromises: Promise<AttendeeId>[];\n\t\t\t\tlet remoteClients: ChildProcess[];\n\t\t\t\tconst workspaceId = \"presenceTestWorkspace\";\n\t\t\t\tconst key1 = \"player1\";\n\t\t\t\tconst key2 = \"player2\";\n\t\t\t\tconst value1 = { name: \"Alice\", score: 100 };\n\t\t\t\tconst value2 = { name: \"Bob\", score: 200 };\n\n\t\t\t\tbeforeEach(async () => {\n\t\t\t\t\t({ children, childErrorPromise } = await forkChildProcesses(\n\t\t\t\t\t\tnumClients,\n\t\t\t\t\t\tafterCleanUp,\n\t\t\t\t\t));\n\t\t\t\t\t({ containerCreatorAttendeeId, attendeeIdPromises } = await connectChildProcesses(\n\t\t\t\t\t\tchildren,\n\t\t\t\t\t\t{ writeClients: numClients, readyTimeoutMs: childConnectTimeoutMs },\n\t\t\t\t\t));\n\t\t\t\t\tawait Promise.all(attendeeIdPromises);\n\t\t\t\t\tremoteClients = children.filter((_, index) => index !== 0);\n\t\t\t\t\t// NOTE: For testing purposes child clients will expect a LatestMap value of type Record<string, string | number> (StateFactory.latestMap<{ value: Record<string, string | number> }, string>).\n\t\t\t\t\tawait registerWorkspaceOnChildren(children, workspaceId, {\n\t\t\t\t\t\tlatestMap: true,\n\t\t\t\t\t\ttimeoutMs: workspaceRegisterTimeoutMs,\n\t\t\t\t\t});\n\t\t\t\t});\n\n\t\t\t\tit(`allows clients to read LatestMap values from other clients [${numClients} clients]`, async () => {\n\t\t\t\t\t// Setup\n\t\t\t\t\tconst testKey = \"cursor\";\n\t\t\t\t\tconst testValue = { x: 150, y: 300 };\n\t\t\t\t\tconst updateEventsPromise = waitForLatestMapValueUpdates(\n\t\t\t\t\t\tremoteClients,\n\t\t\t\t\t\tworkspaceId,\n\t\t\t\t\t\ttestKey,\n\t\t\t\t\t\tchildErrorPromise,\n\t\t\t\t\t\tstateUpdateTimeoutMs,\n\t\t\t\t\t\t{ fromAttendeeId: containerCreatorAttendeeId, expectedValue: testValue },\n\t\t\t\t\t);\n\n\t\t\t\t\t// Act\n\t\t\t\t\tchildren[0].send({\n\t\t\t\t\t\tcommand: \"setLatestMapValue\",\n\t\t\t\t\t\tworkspaceId,\n\t\t\t\t\t\tkey: testKey,\n\t\t\t\t\t\tvalue: testValue,\n\t\t\t\t\t});\n\t\t\t\t\tconst updateEvents = await updateEventsPromise;\n\n\t\t\t\t\t// Check all events are from the expected attendee\n\t\t\t\t\tfor (const updateEvent of updateEvents) {\n\t\t\t\t\t\tassert.strictEqual(updateEvent.attendeeId, containerCreatorAttendeeId);\n\t\t\t\t\t\tassert.strictEqual(updateEvent.key, testKey);\n\t\t\t\t\t\tassert.deepStrictEqual(updateEvent.value, testValue);\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const child of remoteClients) {\n\t\t\t\t\t\tchild.send({\n\t\t\t\t\t\t\tcommand: \"getLatestMapValue\",\n\t\t\t\t\t\t\tworkspaceId,\n\t\t\t\t\t\t\tkey: testKey,\n\t\t\t\t\t\t\tattendeeId: containerCreatorAttendeeId,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tconst getResponses = await getLatestMapValueResponses(\n\t\t\t\t\t\tremoteClients,\n\t\t\t\t\t\tworkspaceId,\n\t\t\t\t\t\ttestKey,\n\t\t\t\t\t\tchildErrorPromise,\n\t\t\t\t\t\tgetStateTimeoutMs,\n\t\t\t\t\t);\n\n\t\t\t\t\t// Verify\n\t\t\t\t\tfor (const getResponse of getResponses) {\n\t\t\t\t\t\tassert.deepStrictEqual(getResponse.value, testValue);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\tit(`returns per-key values on read [${numClients} clients]`, async () => {\n\t\t\t\t\t// Setup\n\t\t\t\t\tconst allAttendeeIds = await Promise.all(attendeeIdPromises);\n\t\t\t\t\tconst attendee0Id = containerCreatorAttendeeId;\n\t\t\t\t\tconst attendee1Id = allAttendeeIds[1];\n\n\t\t\t\t\tconst key1Recipients = children.filter((_, index) => index !== 0);\n\t\t\t\t\tconst key2Recipients = children.filter((_, index) => index !== 1);\n\t\t\t\t\tconst key1UpdateEventsPromise = waitForLatestMapValueUpdates(\n\t\t\t\t\t\tkey1Recipients,\n\t\t\t\t\t\tworkspaceId,\n\t\t\t\t\t\tkey1,\n\t\t\t\t\t\tchildErrorPromise,\n\t\t\t\t\t\tstateUpdateTimeoutMs,\n\t\t\t\t\t\t{ fromAttendeeId: attendee0Id, expectedValue: value1 },\n\t\t\t\t\t);\n\t\t\t\t\tconst key2UpdateEventsPromise = waitForLatestMapValueUpdates(\n\t\t\t\t\t\tkey2Recipients,\n\t\t\t\t\t\tworkspaceId,\n\t\t\t\t\t\tkey2,\n\t\t\t\t\t\tchildErrorPromise,\n\t\t\t\t\t\tstateUpdateTimeoutMs,\n\t\t\t\t\t\t{ fromAttendeeId: attendee1Id, expectedValue: value2 },\n\t\t\t\t\t);\n\n\t\t\t\t\t// Act\n\t\t\t\t\tchildren[0].send({\n\t\t\t\t\t\tcommand: \"setLatestMapValue\",\n\t\t\t\t\t\tworkspaceId,\n\t\t\t\t\t\tkey: key1,\n\t\t\t\t\t\tvalue: value1,\n\t\t\t\t\t});\n\t\t\t\t\tconst key1UpdateEvents = await key1UpdateEventsPromise;\n\t\t\t\t\tchildren[1].send({\n\t\t\t\t\t\tcommand: \"setLatestMapValue\",\n\t\t\t\t\t\tworkspaceId,\n\t\t\t\t\t\tkey: key2,\n\t\t\t\t\t\tvalue: value2,\n\t\t\t\t\t});\n\t\t\t\t\tconst key2UpdateEvents = await key2UpdateEventsPromise;\n\n\t\t\t\t\t// Verify all events are from the expected attendees\n\t\t\t\t\tfor (const updateEvent of key1UpdateEvents) {\n\t\t\t\t\t\tassert.strictEqual(updateEvent.attendeeId, attendee0Id);\n\t\t\t\t\t\tassert.strictEqual(updateEvent.key, key1);\n\t\t\t\t\t\tassert.deepStrictEqual(updateEvent.value, value1);\n\t\t\t\t\t}\n\t\t\t\t\tfor (const updateEvent of key2UpdateEvents) {\n\t\t\t\t\t\tassert.strictEqual(updateEvent.attendeeId, attendee1Id);\n\t\t\t\t\t\tassert.strictEqual(updateEvent.key, key2);\n\t\t\t\t\t\tassert.deepStrictEqual(updateEvent.value, value2);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Read key1 of attendee0 from all children\n\t\t\t\t\tfor (const child of children) {\n\t\t\t\t\t\tchild.send({\n\t\t\t\t\t\t\tcommand: \"getLatestMapValue\",\n\t\t\t\t\t\t\tworkspaceId,\n\t\t\t\t\t\t\tkey: key1,\n\t\t\t\t\t\t\tattendeeId: attendee0Id,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tconst key1Responses = await getLatestMapValueResponses(\n\t\t\t\t\t\tchildren,\n\t\t\t\t\t\tworkspaceId,\n\t\t\t\t\t\tkey1,\n\t\t\t\t\t\tchildErrorPromise,\n\t\t\t\t\t\tgetStateTimeoutMs,\n\t\t\t\t\t);\n\n\t\t\t\t\t// Read key2 of attendee1 from all children\n\t\t\t\t\tfor (const child of children) {\n\t\t\t\t\t\tchild.send({\n\t\t\t\t\t\t\tcommand: \"getLatestMapValue\",\n\t\t\t\t\t\t\tworkspaceId,\n\t\t\t\t\t\t\tkey: key2,\n\t\t\t\t\t\t\tattendeeId: attendee1Id,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tconst key2Responses = await getLatestMapValueResponses(\n\t\t\t\t\t\tchildren,\n\t\t\t\t\t\tworkspaceId,\n\t\t\t\t\t\tkey2,\n\t\t\t\t\t\tchildErrorPromise,\n\t\t\t\t\t\tgetStateTimeoutMs,\n\t\t\t\t\t);\n\n\t\t\t\t\t// Verify\n\t\t\t\t\tassert.strictEqual(\n\t\t\t\t\t\tkey1Responses.length,\n\t\t\t\t\t\tnumClients,\n\t\t\t\t\t\t\"Expected responses from all clients for key1\",\n\t\t\t\t\t);\n\t\t\t\t\tassert.strictEqual(\n\t\t\t\t\t\tkey2Responses.length,\n\t\t\t\t\t\tnumClients,\n\t\t\t\t\t\t\"Expected responses from all clients for key2\",\n\t\t\t\t\t);\n\n\t\t\t\t\tfor (const response of key1Responses) {\n\t\t\t\t\t\tassert.deepStrictEqual(response.value, value1, \"Key1 value should match\");\n\t\t\t\t\t}\n\t\t\t\t\tfor (const response of key2Responses) {\n\t\t\t\t\t\tassert.deepStrictEqual(response.value, value2, \"Key2 value should match\");\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\t}\n});\n"]}