@fluidframework/azure-end-to-end-tests 2.53.0-350190 → 2.53.0
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.
- package/CHANGELOG.md +4 -0
- package/lib/test/multiprocess/childClient.js +9 -3
- package/lib/test/multiprocess/childClient.js.map +1 -1
- package/lib/test/multiprocess/messageTypes.js.map +1 -1
- package/lib/test/multiprocess/presenceTest.spec.js +203 -104
- package/lib/test/multiprocess/presenceTest.spec.js.map +1 -1
- package/package.json +23 -23
- package/src/test/multiprocess/childClient.ts +9 -3
- package/src/test/multiprocess/messageTypes.ts +62 -10
- package/src/test/multiprocess/presenceTest.spec.ts +282 -123
package/CHANGELOG.md
CHANGED
|
@@ -6,9 +6,11 @@ import { strict as assert } from "node:assert";
|
|
|
6
6
|
import { AzureClient, } from "@fluidframework/azure-client";
|
|
7
7
|
import { AttachState } from "@fluidframework/container-definitions";
|
|
8
8
|
import { ConnectionState } from "@fluidframework/container-loader";
|
|
9
|
-
import { getPresence, ExperimentalPresenceManager,
|
|
10
9
|
// eslint-disable-next-line import/no-internal-modules
|
|
11
|
-
} from "@fluidframework/presence/alpha";
|
|
10
|
+
import { ExperimentalPresenceManager } from "@fluidframework/presence/alpha";
|
|
11
|
+
import { getPresence,
|
|
12
|
+
// eslint-disable-next-line import/no-internal-modules
|
|
13
|
+
} from "@fluidframework/presence/beta";
|
|
12
14
|
import { InsecureTokenProvider } from "@fluidframework/test-runtime-utils/internal";
|
|
13
15
|
import { timeoutPromise } from "@fluidframework/test-utils/internal";
|
|
14
16
|
import { createAzureTokenProvider } from "../AzureTokenFactory.js";
|
|
@@ -87,6 +89,10 @@ function isConnected(container) {
|
|
|
87
89
|
class MessageHandler {
|
|
88
90
|
async onMessage(msg) {
|
|
89
91
|
switch (msg.command) {
|
|
92
|
+
case "ping": {
|
|
93
|
+
send({ event: "ack" });
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
90
96
|
// Respond to connect command by connecting to Fluid container with the provided user information.
|
|
91
97
|
case "connect": {
|
|
92
98
|
// Check if valid user information has been provided by parent/orchestrator
|
|
@@ -119,7 +125,7 @@ class MessageHandler {
|
|
|
119
125
|
send(m);
|
|
120
126
|
});
|
|
121
127
|
send({
|
|
122
|
-
event: "
|
|
128
|
+
event: "connected",
|
|
123
129
|
containerId,
|
|
124
130
|
attendeeId: presence.attendees.getMyself().attendeeId,
|
|
125
131
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"childClient.js","sourceRoot":"","sources":["../../../src/test/multiprocess/childClient.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\";\n\nimport {\n\tAzureClient,\n\ttype AzureContainerServices,\n\ttype AzureLocalConnectionConfig,\n\ttype AzureRemoteConnectionConfig,\n} from \"@fluidframework/azure-client\";\nimport { AttachState } from \"@fluidframework/container-definitions\";\nimport { ConnectionState } from \"@fluidframework/container-loader\";\nimport type { ContainerSchema, IFluidContainer } from \"@fluidframework/fluid-static\";\nimport {\n\tgetPresence,\n\ttype Attendee,\n\tExperimentalPresenceManager,\n\ttype Presence,\n\t// eslint-disable-next-line import/no-internal-modules\n} from \"@fluidframework/presence/alpha\";\nimport { InsecureTokenProvider } from \"@fluidframework/test-runtime-utils/internal\";\nimport { timeoutPromise } from \"@fluidframework/test-utils/internal\";\n\nimport type { ScopeType } from \"../AzureClientFactory.js\";\nimport { createAzureTokenProvider } from \"../AzureTokenFactory.js\";\nimport type { configProvider } from \"../utils.js\";\n\nimport type { MessageFromChild, MessageToChild } from \"./messageTypes.js\";\n\ntype MessageFromParent = MessageToChild;\ntype MessageToParent = Required<MessageFromChild>;\ninterface UserIdAndName {\n\tid: string;\n\tname: string;\n}\n\nconst connectTimeoutMs = 10_000;\n// Identifier given to child process\nconst process_id = process.argv[2];\n\nconst useAzure = process.env.FLUID_CLIENT === \"azure\";\nconst tenantId = useAzure\n\t? (process.env.azure__fluid__relay__service__tenantId as string)\n\t: \"frs-client-tenant\";\nconst endPoint = process.env.azure__fluid__relay__service__endpoint as string;\nif (useAzure && endPoint === undefined) {\n\tthrow new Error(\"Azure Fluid Relay service endpoint is missing\");\n}\n\n/**\n * Get or create a Fluid container with Presence in initialObjects.\n */\nconst getOrCreatePresenceContainer = async (\n\tid: string | undefined,\n\tuser: UserIdAndName,\n\tconfig?: ReturnType<typeof configProvider>,\n\tscopes?: ScopeType[],\n): Promise<{\n\tcontainer: IFluidContainer;\n\tpresence: Presence;\n\tservices: AzureContainerServices;\n\tclient: AzureClient;\n\tcontainerId: string;\n}> => {\n\tlet container: IFluidContainer;\n\tlet containerId: string;\n\tconst connectionProps: AzureRemoteConnectionConfig | AzureLocalConnectionConfig = useAzure\n\t\t? {\n\t\t\t\ttenantId,\n\t\t\t\ttokenProvider: createAzureTokenProvider(user.id ?? \"foo\", user.name ?? \"bar\", scopes),\n\t\t\t\tendpoint: endPoint,\n\t\t\t\ttype: \"remote\",\n\t\t\t}\n\t\t: {\n\t\t\t\ttokenProvider: new InsecureTokenProvider(\"fooBar\", user, scopes),\n\t\t\t\tendpoint: \"http://localhost:7071\",\n\t\t\t\ttype: \"local\",\n\t\t\t};\n\tconst client = new AzureClient({ connection: connectionProps });\n\tconst schema: ContainerSchema = {\n\t\tinitialObjects: {\n\t\t\t// A DataObject is added as otherwise fluid-static complains \"Container cannot be initialized without any DataTypes\"\n\t\t\t_unused: ExperimentalPresenceManager,\n\t\t},\n\t};\n\tlet services: AzureContainerServices;\n\tif (id === undefined) {\n\t\t({ container, services } = await client.createContainer(schema, \"2\"));\n\t\tcontainerId = await container.attach();\n\t} else {\n\t\tcontainerId = id;\n\t\t({ container, services } = await client.getContainer(containerId, schema, \"2\"));\n\t}\n\t// wait for 'ConnectionState.Connected' so we return with client connected to container\n\tif (container.connectionState !== ConnectionState.Connected) {\n\t\tawait timeoutPromise((resolve) => container.once(\"connected\", () => resolve()), {\n\t\t\tdurationMs: connectTimeoutMs,\n\t\t\terrorMsg: \"container connect() timeout\",\n\t\t});\n\t}\n\tassert.strictEqual(\n\t\tcontainer.attachState,\n\t\tAttachState.Attached,\n\t\t\"Container is not attached after attach is called\",\n\t);\n\n\tconst presence = getPresence(container);\n\treturn {\n\t\tclient,\n\t\tcontainer,\n\t\tpresence,\n\t\tservices,\n\t\tcontainerId,\n\t};\n};\nfunction createSendFunction(): (msg: MessageToParent) => void {\n\tif (process.send) {\n\t\treturn process.send.bind(process);\n\t}\n\tthrow new Error(\"process.send is not defined\");\n}\n\nconst send = createSendFunction();\n\nfunction isConnected(container: IFluidContainer | undefined): boolean {\n\treturn container !== undefined && container.connectionState === ConnectionState.Connected;\n}\n\nclass MessageHandler {\n\tpublic presence: Presence | undefined;\n\tpublic container: IFluidContainer | undefined;\n\tpublic containerId: string | undefined;\n\n\tpublic async onMessage(msg: MessageFromParent): Promise<void> {\n\t\tswitch (msg.command) {\n\t\t\t// Respond to connect command by connecting to Fluid container with the provided user information.\n\t\t\tcase \"connect\": {\n\t\t\t\t// Check if valid user information has been provided by parent/orchestrator\n\t\t\t\tif (!msg.user) {\n\t\t\t\t\tsend({ event: \"error\", error: `${process_id}: No azure user information given` });\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t// Check if already connected to container\n\t\t\t\tif (isConnected(this.container)) {\n\t\t\t\t\tsend({ event: \"error\", error: `${process_id}: Already connected to container` });\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tconst { container, presence, containerId } = await getOrCreatePresenceContainer(\n\t\t\t\t\tmsg.containerId,\n\t\t\t\t\tmsg.user,\n\t\t\t\t);\n\t\t\t\tthis.container = container;\n\t\t\t\tthis.presence = presence;\n\t\t\t\tthis.containerId = containerId;\n\n\t\t\t\t// Listen for presence events to notify parent/orchestrator when a new attendee joins or leaves the session.\n\t\t\t\tpresence.attendees.events.on(\"attendeeConnected\", (attendee: Attendee) => {\n\t\t\t\t\tconst m: MessageToParent = {\n\t\t\t\t\t\tevent: \"attendeeConnected\",\n\t\t\t\t\t\tattendeeId: attendee.attendeeId,\n\t\t\t\t\t};\n\t\t\t\t\tsend(m);\n\t\t\t\t});\n\t\t\t\tpresence.attendees.events.on(\"attendeeDisconnected\", (attendee: Attendee) => {\n\t\t\t\t\tconst m: MessageToParent = {\n\t\t\t\t\t\tevent: \"attendeeDisconnected\",\n\t\t\t\t\t\tattendeeId: attendee.attendeeId,\n\t\t\t\t\t};\n\t\t\t\t\tsend(m);\n\t\t\t\t});\n\t\t\t\tsend({\n\t\t\t\t\tevent: \"ready\",\n\t\t\t\t\tcontainerId,\n\t\t\t\t\tattendeeId: presence.attendees.getMyself().attendeeId,\n\t\t\t\t});\n\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Respond to disconnect command by disconnecting self from Fluid container.\n\t\t\tcase \"disconnectSelf\": {\n\t\t\t\tif (!this.container) {\n\t\t\t\t\tsend({ event: \"error\", error: `${process_id} is not connected to container` });\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (!this.presence) {\n\t\t\t\t\tsend({ event: \"error\", error: `${process_id} is not connected to presence` });\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tthis.container.disconnect();\n\t\t\t\tsend({\n\t\t\t\t\tevent: \"disconnectedSelf\",\n\t\t\t\t\tattendeeId: this.presence.attendees.getMyself().attendeeId,\n\t\t\t\t});\n\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\tconsole.error(`${process_id}: Unknown command`);\n\t\t\t\tsend({ event: \"error\", error: `${process_id} Unknown command` });\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction setupMessageHandler(): void {\n\tconst messageHandler = new MessageHandler();\n\tprocess.on(\"message\", (msg: MessageFromParent) => {\n\t\tmessageHandler.onMessage(msg).catch((error: Error) => {\n\t\t\tconsole.error(`Error in client ${process_id}`, error);\n\t\t\tsend({ event: \"error\", error: `${process_id}: ${error.message}` });\n\t\t});\n\t});\n}\n\nsetupMessageHandler();\n"]}
|
|
1
|
+
{"version":3,"file":"childClient.js","sourceRoot":"","sources":["../../../src/test/multiprocess/childClient.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\";\n\nimport {\n\tAzureClient,\n\ttype AzureContainerServices,\n\ttype AzureLocalConnectionConfig,\n\ttype AzureRemoteConnectionConfig,\n} from \"@fluidframework/azure-client\";\nimport { AttachState } from \"@fluidframework/container-definitions\";\nimport { ConnectionState } from \"@fluidframework/container-loader\";\nimport type { ContainerSchema, IFluidContainer } from \"@fluidframework/fluid-static\";\n// eslint-disable-next-line import/no-internal-modules\nimport { ExperimentalPresenceManager } from \"@fluidframework/presence/alpha\";\nimport {\n\tgetPresence,\n\ttype Attendee,\n\ttype Presence,\n\t// eslint-disable-next-line import/no-internal-modules\n} from \"@fluidframework/presence/beta\";\nimport { InsecureTokenProvider } from \"@fluidframework/test-runtime-utils/internal\";\nimport { timeoutPromise } from \"@fluidframework/test-utils/internal\";\n\nimport type { ScopeType } from \"../AzureClientFactory.js\";\nimport { createAzureTokenProvider } from \"../AzureTokenFactory.js\";\nimport type { configProvider } from \"../utils.js\";\n\nimport type { MessageFromChild, MessageToChild } from \"./messageTypes.js\";\n\ntype MessageFromParent = MessageToChild;\ntype MessageToParent = Required<MessageFromChild>;\ninterface UserIdAndName {\n\tid: string;\n\tname: string;\n}\n\nconst connectTimeoutMs = 10_000;\n// Identifier given to child process\nconst process_id = process.argv[2];\n\nconst useAzure = process.env.FLUID_CLIENT === \"azure\";\nconst tenantId = useAzure\n\t? (process.env.azure__fluid__relay__service__tenantId as string)\n\t: \"frs-client-tenant\";\nconst endPoint = process.env.azure__fluid__relay__service__endpoint as string;\nif (useAzure && endPoint === undefined) {\n\tthrow new Error(\"Azure Fluid Relay service endpoint is missing\");\n}\n\n/**\n * Get or create a Fluid container with Presence in initialObjects.\n */\nconst getOrCreatePresenceContainer = async (\n\tid: string | undefined,\n\tuser: UserIdAndName,\n\tconfig?: ReturnType<typeof configProvider>,\n\tscopes?: ScopeType[],\n): Promise<{\n\tcontainer: IFluidContainer;\n\tpresence: Presence;\n\tservices: AzureContainerServices;\n\tclient: AzureClient;\n\tcontainerId: string;\n}> => {\n\tlet container: IFluidContainer;\n\tlet containerId: string;\n\tconst connectionProps: AzureRemoteConnectionConfig | AzureLocalConnectionConfig = useAzure\n\t\t? {\n\t\t\t\ttenantId,\n\t\t\t\ttokenProvider: createAzureTokenProvider(user.id ?? \"foo\", user.name ?? \"bar\", scopes),\n\t\t\t\tendpoint: endPoint,\n\t\t\t\ttype: \"remote\",\n\t\t\t}\n\t\t: {\n\t\t\t\ttokenProvider: new InsecureTokenProvider(\"fooBar\", user, scopes),\n\t\t\t\tendpoint: \"http://localhost:7071\",\n\t\t\t\ttype: \"local\",\n\t\t\t};\n\tconst client = new AzureClient({ connection: connectionProps });\n\tconst schema: ContainerSchema = {\n\t\tinitialObjects: {\n\t\t\t// A DataObject is added as otherwise fluid-static complains \"Container cannot be initialized without any DataTypes\"\n\t\t\t_unused: ExperimentalPresenceManager,\n\t\t},\n\t};\n\tlet services: AzureContainerServices;\n\tif (id === undefined) {\n\t\t({ container, services } = await client.createContainer(schema, \"2\"));\n\t\tcontainerId = await container.attach();\n\t} else {\n\t\tcontainerId = id;\n\t\t({ container, services } = await client.getContainer(containerId, schema, \"2\"));\n\t}\n\t// wait for 'ConnectionState.Connected' so we return with client connected to container\n\tif (container.connectionState !== ConnectionState.Connected) {\n\t\tawait timeoutPromise((resolve) => container.once(\"connected\", () => resolve()), {\n\t\t\tdurationMs: connectTimeoutMs,\n\t\t\terrorMsg: \"container connect() timeout\",\n\t\t});\n\t}\n\tassert.strictEqual(\n\t\tcontainer.attachState,\n\t\tAttachState.Attached,\n\t\t\"Container is not attached after attach is called\",\n\t);\n\n\tconst presence = getPresence(container);\n\treturn {\n\t\tclient,\n\t\tcontainer,\n\t\tpresence,\n\t\tservices,\n\t\tcontainerId,\n\t};\n};\nfunction createSendFunction(): (msg: MessageToParent) => void {\n\tif (process.send) {\n\t\treturn process.send.bind(process);\n\t}\n\tthrow new Error(\"process.send is not defined\");\n}\n\nconst send = createSendFunction();\n\nfunction isConnected(container: IFluidContainer | undefined): boolean {\n\treturn container !== undefined && container.connectionState === ConnectionState.Connected;\n}\n\nclass MessageHandler {\n\tpublic presence: Presence | undefined;\n\tpublic container: IFluidContainer | undefined;\n\tpublic containerId: string | undefined;\n\n\tpublic async onMessage(msg: MessageFromParent): Promise<void> {\n\t\tswitch (msg.command) {\n\t\t\tcase \"ping\": {\n\t\t\t\tsend({ event: \"ack\" });\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Respond to connect command by connecting to Fluid container with the provided user information.\n\t\t\tcase \"connect\": {\n\t\t\t\t// Check if valid user information has been provided by parent/orchestrator\n\t\t\t\tif (!msg.user) {\n\t\t\t\t\tsend({ event: \"error\", error: `${process_id}: No azure user information given` });\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t// Check if already connected to container\n\t\t\t\tif (isConnected(this.container)) {\n\t\t\t\t\tsend({ event: \"error\", error: `${process_id}: Already connected to container` });\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tconst { container, presence, containerId } = await getOrCreatePresenceContainer(\n\t\t\t\t\tmsg.containerId,\n\t\t\t\t\tmsg.user,\n\t\t\t\t);\n\t\t\t\tthis.container = container;\n\t\t\t\tthis.presence = presence;\n\t\t\t\tthis.containerId = containerId;\n\n\t\t\t\t// Listen for presence events to notify parent/orchestrator when a new attendee joins or leaves the session.\n\t\t\t\tpresence.attendees.events.on(\"attendeeConnected\", (attendee: Attendee) => {\n\t\t\t\t\tconst m: MessageToParent = {\n\t\t\t\t\t\tevent: \"attendeeConnected\",\n\t\t\t\t\t\tattendeeId: attendee.attendeeId,\n\t\t\t\t\t};\n\t\t\t\t\tsend(m);\n\t\t\t\t});\n\t\t\t\tpresence.attendees.events.on(\"attendeeDisconnected\", (attendee: Attendee) => {\n\t\t\t\t\tconst m: MessageToParent = {\n\t\t\t\t\t\tevent: \"attendeeDisconnected\",\n\t\t\t\t\t\tattendeeId: attendee.attendeeId,\n\t\t\t\t\t};\n\t\t\t\t\tsend(m);\n\t\t\t\t});\n\t\t\t\tsend({\n\t\t\t\t\tevent: \"connected\",\n\t\t\t\t\tcontainerId,\n\t\t\t\t\tattendeeId: presence.attendees.getMyself().attendeeId,\n\t\t\t\t});\n\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Respond to disconnect command by disconnecting self from Fluid container.\n\t\t\tcase \"disconnectSelf\": {\n\t\t\t\tif (!this.container) {\n\t\t\t\t\tsend({ event: \"error\", error: `${process_id} is not connected to container` });\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (!this.presence) {\n\t\t\t\t\tsend({ event: \"error\", error: `${process_id} is not connected to presence` });\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tthis.container.disconnect();\n\t\t\t\tsend({\n\t\t\t\t\tevent: \"disconnectedSelf\",\n\t\t\t\t\tattendeeId: this.presence.attendees.getMyself().attendeeId,\n\t\t\t\t});\n\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\tconsole.error(`${process_id}: Unknown command`);\n\t\t\t\tsend({ event: \"error\", error: `${process_id} Unknown command` });\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction setupMessageHandler(): void {\n\tconst messageHandler = new MessageHandler();\n\tprocess.on(\"message\", (msg: MessageFromParent) => {\n\t\tmessageHandler.onMessage(msg).catch((error: Error) => {\n\t\t\tconsole.error(`Error in client ${process_id}`, error);\n\t\t\tsend({ event: \"error\", error: `${process_id}: ${error.message}` });\n\t\t});\n\t});\n}\n\nsetupMessageHandler();\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messageTypes.js","sourceRoot":"","sources":["../../../src/test/multiprocess/messageTypes.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport type { AzureUser } from \"@fluidframework/azure-client/internal\";\n// eslint-disable-next-line import/no-internal-modules\nimport type { AttendeeId } from \"@fluidframework/presence/beta\";\n\nexport type MessageToChild = ConnectCommand | DisconnectSelfCommand;\ninterface ConnectCommand {\n\tcommand: \"connect\";\n\tuser: AzureUser;\n\tcontainerId?: string;\n}\n\ninterface DisconnectSelfCommand {\n\tcommand: \"disconnectSelf\";\n}\n\nexport type MessageFromChild =\n\t|
|
|
1
|
+
{"version":3,"file":"messageTypes.js","sourceRoot":"","sources":["../../../src/test/multiprocess/messageTypes.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport type { AzureUser } from \"@fluidframework/azure-client/internal\";\n// eslint-disable-next-line import/no-internal-modules\nimport type { AttendeeId } from \"@fluidframework/presence/beta\";\n\n/**\n * Message types sent from the orchestrator to the child processes\n */\nexport type MessageToChild = ConnectCommand | DisconnectSelfCommand | PingCommand;\n\n/**\n * Can be sent to check child responsiveness.\n * An {@link AcknowledgeEvent} should be expected in response.\n */\ninterface PingCommand {\n\tcommand: \"ping\";\n}\n\n/**\n * Instructs a child process to connect to a Fluid container.\n * A {@link ConnectedEvent} should be expected in response.\n */\nexport interface ConnectCommand {\n\tcommand: \"connect\";\n\tuser: AzureUser;\n\t/**\n\t * The ID of the Fluid container to connect to.\n\t * If not provided, a new Fluid container will be created.\n\t */\n\tcontainerId?: string;\n}\n\n/**\n * Instructs a child process to disconnect from a Fluid container.\n * A {@link DisconnectedSelfEvent} should be expected in response.\n */\ninterface DisconnectSelfCommand {\n\tcommand: \"disconnectSelf\";\n}\n\n/**\n * Message types sent from the child processes to the orchestrator\n */\nexport type MessageFromChild =\n\t| AcknowledgeEvent\n\t| AttendeeConnectedEvent\n\t| AttendeeDisconnectedEvent\n\t| ConnectedEvent\n\t| DisconnectedSelfEvent\n\t| ErrorEvent;\n\n/**\n * Sent from the child processes to the orchestrator in response to a {@link PingCommand}.\n */\ninterface AcknowledgeEvent {\n\tevent: \"ack\";\n}\n\n/**\n * Sent arbitrarily to indicate a new attendee has connected.\n */\ninterface AttendeeConnectedEvent {\n\tevent: \"attendeeConnected\";\n\tattendeeId: AttendeeId;\n}\n\n/**\n * Sent arbitrarily to indicate an attendee has disconnected.\n */\ninterface AttendeeDisconnectedEvent {\n\tevent: \"attendeeDisconnected\";\n\tattendeeId: AttendeeId;\n}\n\n/**\n * Sent from the child processes to the orchestrator in response to a {@link ConnectCommand}.\n */\ninterface ConnectedEvent {\n\tevent: \"connected\";\n\tcontainerId: string;\n\tattendeeId: AttendeeId;\n}\n\n/**\n * Sent from the child processes to the orchestrator in response to a {@link DisconnectSelfCommand}.\n */\ninterface DisconnectedSelfEvent {\n\tevent: \"disconnectedSelf\";\n\tattendeeId: AttendeeId;\n}\n\n/**\n * Sent at any time to indicate an error.\n */\ninterface ErrorEvent {\n\tevent: \"error\";\n\terror: string;\n}\n"]}
|
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { strict as assert } from "node:assert";
|
|
6
6
|
import { fork } from "node:child_process";
|
|
7
|
-
import { timeoutPromise } from "@fluidframework/test-utils/internal";
|
|
7
|
+
import { timeoutAwait, timeoutPromise } from "@fluidframework/test-utils/internal";
|
|
8
8
|
/**
|
|
9
9
|
* This test suite is a prototype for a multi-process end to end test for Fluid using the new Presence API on AzureClient.
|
|
10
10
|
* In the future we hope to expand and generalize this pattern to broadly test more Fluid features.
|
|
11
|
-
*
|
|
12
|
-
* simulate real-world production scenarios where clients are usually running on different machines.
|
|
11
|
+
* Other E2E tests are limited to running multiple clients on a single process which does not effectively
|
|
12
|
+
* simulate real-world production scenarios where clients are usually running on different machines. Since
|
|
13
|
+
* the Fluid Framework client is designed to carry most of the work burden, multi-process testing from a
|
|
14
|
+
* single machine is also not representative but does at least work past some limitations of a single
|
|
15
|
+
* Node.js process handling multiple clients.
|
|
13
16
|
*
|
|
14
17
|
* The pattern demonstrated in this test suite is as follows:
|
|
15
18
|
*
|
|
@@ -26,122 +29,218 @@ import { timeoutPromise } from "@fluidframework/test-utils/internal";
|
|
|
26
29
|
* - Listen for command messages from the orchestrator.
|
|
27
30
|
* - Perform the requested action.
|
|
28
31
|
* - Send response messages including any relevant data back to the orchestrator to verify expected behavior.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Fork child processes to simulate multiple Fluid clients.
|
|
29
35
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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.
|
|
33
47
|
*/
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
let
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
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,
|
|
65
112
|
});
|
|
66
113
|
}
|
|
67
114
|
else {
|
|
68
|
-
|
|
69
|
-
message.containerId = await containerIdPromise;
|
|
115
|
+
reject(new Error(`Non-connected message from child0: ${JSON.stringify(msg)}`));
|
|
70
116
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
{
|
|
120
|
+
firstChild.send(composeConnectMessage(0));
|
|
74
121
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
reject(new Error(`Child${i} process errored: ${error.message}`));
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
childErrorPromises.push(errorPromise);
|
|
89
|
-
children.push(child);
|
|
90
|
-
// Register cleanup for the child process listeners.
|
|
91
|
-
afterCleanUp.push(() => child.removeAllListeners());
|
|
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;
|
|
92
132
|
}
|
|
93
|
-
|
|
94
|
-
|
|
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);
|
|
95
184
|
});
|
|
96
|
-
//
|
|
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
|
+
/**
|
|
193
|
+
* This particular test suite tests the following E2E functionality for Presence:
|
|
194
|
+
* - Announce 'attendeeConnected' when remote client joins session.
|
|
195
|
+
* - Announce 'attendeeDisconnected' when remote client disconnects.
|
|
196
|
+
*/
|
|
197
|
+
describe(`Presence with AzureClient`, () => {
|
|
198
|
+
const afterCleanUp = [];
|
|
199
|
+
// After each test, call any cleanup functions that were registered (kill each child process)
|
|
97
200
|
afterEach(async () => {
|
|
98
|
-
for (const child of children) {
|
|
99
|
-
child.kill();
|
|
100
|
-
}
|
|
101
|
-
children = [];
|
|
102
201
|
for (const cleanUp of afterCleanUp) {
|
|
103
202
|
cleanUp();
|
|
104
203
|
}
|
|
105
204
|
afterCleanUp.length = 0;
|
|
106
205
|
});
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
durationMs,
|
|
121
|
-
errorMsg: "did not receive all 'attendeeConnected' events",
|
|
206
|
+
// Note that on slower systems 50+ clients may take too long to join.
|
|
207
|
+
const numClientsForAttendeeTests = [5, 20, 50, 100];
|
|
208
|
+
// TODO: AB#45620: "Presence: perf: update Join pattern for scale" may help, then remove .slice.
|
|
209
|
+
for (const numClients of numClientsForAttendeeTests.slice(0, 2)) {
|
|
210
|
+
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);
|
|
122
219
|
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
}
|
|
234
|
+
});
|
|
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]);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
146
245
|
});
|
|
147
246
|
//# sourceMappingURL=presenceTest.spec.js.map
|
|
@@ -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\nimport { timeoutPromise } from \"@fluidframework/test-utils/internal\";\n\nimport type { MessageFromChild, MessageToChild } 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 * Currently our 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.\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 * 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 numClients = 5; // Set the total number of Fluid clients to create\n\tassert(numClients > 1, \"Must have at least two clients\");\n\tlet children: ChildProcess[] = [];\n\t// This promise is used to capture all errors that occur in the child processes.\n\t// It will never resolve successfully, it is only used to reject on child process error.\n\tlet childErrorPromise: Promise<void>;\n\t// Timeout duration used when waiting for response messages from child processes.\n\tconst durationMs = 10_000;\n\n\tconst afterCleanUp: (() => void)[] = [];\n\n\tasync function connectChildProcesses(\n\t\tchildProcesses: ChildProcess[],\n\t): Promise<string | undefined> {\n\t\tlet containerIdPromise: Promise<string> | undefined;\n\t\tlet containerCreatorSessionId: string | undefined;\n\t\tfor (const [index, child] of childProcesses.entries()) {\n\t\t\tconst user = { id: `test-user-id-${index}`, name: `test-user-name-${index}` };\n\t\t\tconst message: MessageToChild = { command: \"connect\", user };\n\n\t\t\tif (containerIdPromise === undefined) {\n\t\t\t\t// Create a promise that resolves with the containerId from the created container.\n\t\t\t\tcontainerIdPromise = timeoutPromise<string>(\n\t\t\t\t\t(resolve, reject) => {\n\t\t\t\t\t\tchild.once(\"message\", (msg: MessageFromChild) => {\n\t\t\t\t\t\t\tif (msg.event === \"ready\" && msg.containerId) {\n\t\t\t\t\t\t\t\tcontainerCreatorSessionId = msg.attendeeId;\n\t\t\t\t\t\t\t\tresolve(msg.containerId);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\treject(new Error(`Non-ready message from child0: ${JSON.stringify(msg)}`));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tdurationMs,\n\t\t\t\t\t\terrorMsg: \"did not receive 'ready' from child process\",\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\t// For subsequent children, wait for containerId from the promise only when needed.\n\t\t\t\tmessage.containerId = await containerIdPromise;\n\t\t\t}\n\n\t\t\tchild.send(message);\n\t\t}\n\t\treturn containerCreatorSessionId;\n\t}\n\n\tbeforeEach(\"setup\", async () => {\n\t\t// Collect all child process error promises into this array\n\t\tconst childErrorPromises: Promise<void>[] = [];\n\t\t// Fork child processes\n\t\tfor (let i = 0; i < numClients; i++) {\n\t\t\tconst child = fork(\"./lib/test/multiprocess/childClient.js\", [\n\t\t\t\t`child${i}` /* identifier passed to child process */,\n\t\t\t]);\n\t\t\tconst errorPromise = new Promise<void>((_, reject) => {\n\t\t\t\tchild.on(\"error\", (error) => {\n\t\t\t\t\treject(new Error(`Child${i} process errored: ${error.message}`));\n\t\t\t\t});\n\t\t\t});\n\t\t\tchildErrorPromises.push(errorPromise);\n\t\t\tchildren.push(child);\n\t\t\t// Register cleanup for the child process listeners.\n\t\t\tafterCleanUp.push(() => child.removeAllListeners());\n\t\t}\n\t\t// This race will be used to reject any of the following tests on any child process errors\n\t\tchildErrorPromise = Promise.race(childErrorPromises);\n\t});\n\n\t// After each test, kill each child process and call any cleanup functions that were registered\n\tafterEach(async () => {\n\t\tfor (const child of children) {\n\t\t\tchild.kill();\n\t\t}\n\t\tchildren = [];\n\t\tfor (const cleanUp of afterCleanUp) {\n\t\t\tcleanUp();\n\t\t}\n\t\tafterCleanUp.length = 0;\n\t});\n\n\tit(\"announces 'attendeeConnected' when remote client joins session and 'attendeeDisconnected' when remote client disconnects\", async () => {\n\t\t// Setup\n\t\tconst attendeeConnectedPromise = timeoutPromise(\n\t\t\t(resolve) => {\n\t\t\t\tlet attendeesJoinedEvents = 0;\n\t\t\t\tchildren[0].on(\"message\", (msg: MessageFromChild) => {\n\t\t\t\t\tif (msg.event === \"attendeeConnected\") {\n\t\t\t\t\t\tattendeesJoinedEvents++;\n\t\t\t\t\t\tif (attendeesJoinedEvents === numClients - 1) {\n\t\t\t\t\t\t\tresolve();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t},\n\t\t\t{\n\t\t\t\tdurationMs,\n\t\t\t\terrorMsg: \"did not receive all 'attendeeConnected' events\",\n\t\t\t},\n\t\t);\n\n\t\t// Act - connect all child processes\n\t\tconst creatorSessionId = await connectChildProcesses(children);\n\n\t\t// Verify - wait for all 'attendeeConnected' events\n\t\tawait Promise.race([attendeeConnectedPromise, childErrorPromise]);\n\n\t\t// Setup\n\t\tconst waitForDisconnected = children\n\t\t\t.filter((_, index) => index !== 0)\n\t\t\t.map(async (child, index) =>\n\t\t\t\ttimeoutPromise(\n\t\t\t\t\t(resolve) => {\n\t\t\t\t\t\tchild.on(\"message\", (msg: MessageFromChild) => {\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tmsg.event === \"attendeeDisconnected\" &&\n\t\t\t\t\t\t\t\tmsg.attendeeId === creatorSessionId\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tresolve();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tdurationMs,\n\t\t\t\t\t\terrorMsg: `Attendee[${index}] Disconnected Timeout`,\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t);\n\n\t\t// Act - disconnect first child process\n\t\tchildren[0].send({ command: \"disconnectSelf\" });\n\n\t\t// Verify - wait for all 'attendeeDisconnected' events\n\t\tawait Promise.race([Promise.all(waitForDisconnected), childErrorPromise]);\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 { 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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluidframework/azure-end-to-end-tests",
|
|
3
|
-
"version": "2.53.0
|
|
3
|
+
"version": "2.53.0",
|
|
4
4
|
"description": "Azure client end to end tests",
|
|
5
5
|
"homepage": "https://fluidframework.com",
|
|
6
6
|
"repository": {
|
|
@@ -33,29 +33,29 @@
|
|
|
33
33
|
"temp-directory": "nyc/.nyc_output"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@fluid-experimental/data-objects": "2.53.0
|
|
37
|
-
"@fluid-internal/client-utils": "2.53.0
|
|
38
|
-
"@fluid-internal/mocha-test-setup": "2.53.0
|
|
39
|
-
"@fluid-private/test-version-utils": "2.53.0
|
|
40
|
-
"@fluidframework/aqueduct": "2.53.0
|
|
41
|
-
"@fluidframework/azure-client": "2.53.0
|
|
36
|
+
"@fluid-experimental/data-objects": "~2.53.0",
|
|
37
|
+
"@fluid-internal/client-utils": "~2.53.0",
|
|
38
|
+
"@fluid-internal/mocha-test-setup": "~2.53.0",
|
|
39
|
+
"@fluid-private/test-version-utils": "~2.53.0",
|
|
40
|
+
"@fluidframework/aqueduct": "~2.53.0",
|
|
41
|
+
"@fluidframework/azure-client": "~2.53.0",
|
|
42
42
|
"@fluidframework/azure-client-legacy": "npm:@fluidframework/azure-client@^1.2.0",
|
|
43
|
-
"@fluidframework/container-definitions": "2.53.0
|
|
44
|
-
"@fluidframework/container-loader": "2.53.0
|
|
45
|
-
"@fluidframework/core-interfaces": "2.53.0
|
|
46
|
-
"@fluidframework/counter": "2.53.0
|
|
47
|
-
"@fluidframework/datastore-definitions": "2.53.0
|
|
48
|
-
"@fluidframework/fluid-static": "2.53.0
|
|
49
|
-
"@fluidframework/map": "2.53.0
|
|
43
|
+
"@fluidframework/container-definitions": "~2.53.0",
|
|
44
|
+
"@fluidframework/container-loader": "~2.53.0",
|
|
45
|
+
"@fluidframework/core-interfaces": "~2.53.0",
|
|
46
|
+
"@fluidframework/counter": "~2.53.0",
|
|
47
|
+
"@fluidframework/datastore-definitions": "~2.53.0",
|
|
48
|
+
"@fluidframework/fluid-static": "~2.53.0",
|
|
49
|
+
"@fluidframework/map": "~2.53.0",
|
|
50
50
|
"@fluidframework/map-legacy": "npm:@fluidframework/map@^1.4.0",
|
|
51
|
-
"@fluidframework/matrix": "2.53.0
|
|
52
|
-
"@fluidframework/presence": "2.53.0
|
|
53
|
-
"@fluidframework/runtime-definitions": "2.53.0
|
|
54
|
-
"@fluidframework/sequence": "2.53.0
|
|
55
|
-
"@fluidframework/telemetry-utils": "2.53.0
|
|
56
|
-
"@fluidframework/test-runtime-utils": "2.53.0
|
|
57
|
-
"@fluidframework/test-utils": "2.53.0
|
|
58
|
-
"@fluidframework/tree": "2.53.0
|
|
51
|
+
"@fluidframework/matrix": "~2.53.0",
|
|
52
|
+
"@fluidframework/presence": "~2.53.0",
|
|
53
|
+
"@fluidframework/runtime-definitions": "~2.53.0",
|
|
54
|
+
"@fluidframework/sequence": "~2.53.0",
|
|
55
|
+
"@fluidframework/telemetry-utils": "~2.53.0",
|
|
56
|
+
"@fluidframework/test-runtime-utils": "~2.53.0",
|
|
57
|
+
"@fluidframework/test-utils": "~2.53.0",
|
|
58
|
+
"@fluidframework/tree": "~2.53.0",
|
|
59
59
|
"axios": "^1.8.4",
|
|
60
60
|
"cross-env": "^7.0.3",
|
|
61
61
|
"mocha": "^10.8.2",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"@biomejs/biome": "~1.9.3",
|
|
70
70
|
"@fluidframework/build-common": "^2.0.3",
|
|
71
71
|
"@fluidframework/build-tools": "^0.57.0",
|
|
72
|
-
"@fluidframework/driver-definitions": "2.53.0
|
|
72
|
+
"@fluidframework/driver-definitions": "~2.53.0",
|
|
73
73
|
"@fluidframework/eslint-config-fluid": "^5.7.4",
|
|
74
74
|
"@types/mocha": "^10.0.10",
|
|
75
75
|
"@types/nock": "^9.3.0",
|
|
@@ -14,13 +14,14 @@ import {
|
|
|
14
14
|
import { AttachState } from "@fluidframework/container-definitions";
|
|
15
15
|
import { ConnectionState } from "@fluidframework/container-loader";
|
|
16
16
|
import type { ContainerSchema, IFluidContainer } from "@fluidframework/fluid-static";
|
|
17
|
+
// eslint-disable-next-line import/no-internal-modules
|
|
18
|
+
import { ExperimentalPresenceManager } from "@fluidframework/presence/alpha";
|
|
17
19
|
import {
|
|
18
20
|
getPresence,
|
|
19
21
|
type Attendee,
|
|
20
|
-
ExperimentalPresenceManager,
|
|
21
22
|
type Presence,
|
|
22
23
|
// eslint-disable-next-line import/no-internal-modules
|
|
23
|
-
} from "@fluidframework/presence/
|
|
24
|
+
} from "@fluidframework/presence/beta";
|
|
24
25
|
import { InsecureTokenProvider } from "@fluidframework/test-runtime-utils/internal";
|
|
25
26
|
import { timeoutPromise } from "@fluidframework/test-utils/internal";
|
|
26
27
|
|
|
@@ -136,6 +137,11 @@ class MessageHandler {
|
|
|
136
137
|
|
|
137
138
|
public async onMessage(msg: MessageFromParent): Promise<void> {
|
|
138
139
|
switch (msg.command) {
|
|
140
|
+
case "ping": {
|
|
141
|
+
send({ event: "ack" });
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
|
|
139
145
|
// Respond to connect command by connecting to Fluid container with the provided user information.
|
|
140
146
|
case "connect": {
|
|
141
147
|
// Check if valid user information has been provided by parent/orchestrator
|
|
@@ -172,7 +178,7 @@ class MessageHandler {
|
|
|
172
178
|
send(m);
|
|
173
179
|
});
|
|
174
180
|
send({
|
|
175
|
-
event: "
|
|
181
|
+
event: "connected",
|
|
176
182
|
containerId,
|
|
177
183
|
attendeeId: presence.attendees.getMyself().attendeeId,
|
|
178
184
|
});
|
|
@@ -7,43 +7,95 @@ import type { AzureUser } from "@fluidframework/azure-client/internal";
|
|
|
7
7
|
// eslint-disable-next-line import/no-internal-modules
|
|
8
8
|
import type { AttendeeId } from "@fluidframework/presence/beta";
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Message types sent from the orchestrator to the child processes
|
|
12
|
+
*/
|
|
13
|
+
export type MessageToChild = ConnectCommand | DisconnectSelfCommand | PingCommand;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Can be sent to check child responsiveness.
|
|
17
|
+
* An {@link AcknowledgeEvent} should be expected in response.
|
|
18
|
+
*/
|
|
19
|
+
interface PingCommand {
|
|
20
|
+
command: "ping";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Instructs a child process to connect to a Fluid container.
|
|
25
|
+
* A {@link ConnectedEvent} should be expected in response.
|
|
26
|
+
*/
|
|
27
|
+
export interface ConnectCommand {
|
|
12
28
|
command: "connect";
|
|
13
29
|
user: AzureUser;
|
|
30
|
+
/**
|
|
31
|
+
* The ID of the Fluid container to connect to.
|
|
32
|
+
* If not provided, a new Fluid container will be created.
|
|
33
|
+
*/
|
|
14
34
|
containerId?: string;
|
|
15
35
|
}
|
|
16
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Instructs a child process to disconnect from a Fluid container.
|
|
39
|
+
* A {@link DisconnectedSelfEvent} should be expected in response.
|
|
40
|
+
*/
|
|
17
41
|
interface DisconnectSelfCommand {
|
|
18
42
|
command: "disconnectSelf";
|
|
19
43
|
}
|
|
20
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Message types sent from the child processes to the orchestrator
|
|
47
|
+
*/
|
|
21
48
|
export type MessageFromChild =
|
|
49
|
+
| AcknowledgeEvent
|
|
50
|
+
| AttendeeConnectedEvent
|
|
22
51
|
| AttendeeDisconnectedEvent
|
|
23
|
-
|
|
|
24
|
-
| ReadyEvent
|
|
52
|
+
| ConnectedEvent
|
|
25
53
|
| DisconnectedSelfEvent
|
|
26
54
|
| ErrorEvent;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Sent from the child processes to the orchestrator in response to a {@link PingCommand}.
|
|
58
|
+
*/
|
|
59
|
+
interface AcknowledgeEvent {
|
|
60
|
+
event: "ack";
|
|
30
61
|
}
|
|
31
62
|
|
|
32
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Sent arbitrarily to indicate a new attendee has connected.
|
|
65
|
+
*/
|
|
66
|
+
interface AttendeeConnectedEvent {
|
|
33
67
|
event: "attendeeConnected";
|
|
34
68
|
attendeeId: AttendeeId;
|
|
35
69
|
}
|
|
36
70
|
|
|
37
|
-
|
|
38
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Sent arbitrarily to indicate an attendee has disconnected.
|
|
73
|
+
*/
|
|
74
|
+
interface AttendeeDisconnectedEvent {
|
|
75
|
+
event: "attendeeDisconnected";
|
|
76
|
+
attendeeId: AttendeeId;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Sent from the child processes to the orchestrator in response to a {@link ConnectCommand}.
|
|
81
|
+
*/
|
|
82
|
+
interface ConnectedEvent {
|
|
83
|
+
event: "connected";
|
|
39
84
|
containerId: string;
|
|
40
85
|
attendeeId: AttendeeId;
|
|
41
86
|
}
|
|
42
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Sent from the child processes to the orchestrator in response to a {@link DisconnectSelfCommand}.
|
|
90
|
+
*/
|
|
43
91
|
interface DisconnectedSelfEvent {
|
|
44
92
|
event: "disconnectedSelf";
|
|
45
93
|
attendeeId: AttendeeId;
|
|
46
94
|
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Sent at any time to indicate an error.
|
|
98
|
+
*/
|
|
47
99
|
interface ErrorEvent {
|
|
48
100
|
event: "error";
|
|
49
101
|
error: string;
|
|
@@ -6,15 +6,20 @@
|
|
|
6
6
|
import { strict as assert } from "node:assert";
|
|
7
7
|
import { fork, type ChildProcess } from "node:child_process";
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// eslint-disable-next-line import/no-internal-modules
|
|
10
|
+
import type { AttendeeId } from "@fluidframework/presence/beta";
|
|
11
|
+
import { timeoutAwait, timeoutPromise } from "@fluidframework/test-utils/internal";
|
|
10
12
|
|
|
11
|
-
import type {
|
|
13
|
+
import type { ConnectCommand, MessageFromChild } from "./messageTypes.js";
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* This test suite is a prototype for a multi-process end to end test for Fluid using the new Presence API on AzureClient.
|
|
15
17
|
* In the future we hope to expand and generalize this pattern to broadly test more Fluid features.
|
|
16
|
-
*
|
|
17
|
-
* simulate real-world production scenarios where clients are usually running on different machines.
|
|
18
|
+
* Other E2E tests are limited to running multiple clients on a single process which does not effectively
|
|
19
|
+
* simulate real-world production scenarios where clients are usually running on different machines. Since
|
|
20
|
+
* the Fluid Framework client is designed to carry most of the work burden, multi-process testing from a
|
|
21
|
+
* single machine is also not representative but does at least work past some limitations of a single
|
|
22
|
+
* Node.js process handling multiple clients.
|
|
18
23
|
*
|
|
19
24
|
* The pattern demonstrated in this test suite is as follows:
|
|
20
25
|
*
|
|
@@ -31,146 +36,300 @@ import type { MessageFromChild, MessageToChild } from "./messageTypes.js";
|
|
|
31
36
|
* - Listen for command messages from the orchestrator.
|
|
32
37
|
* - Perform the requested action.
|
|
33
38
|
* - Send response messages including any relevant data back to the orchestrator to verify expected behavior.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Fork child processes to simulate multiple Fluid clients.
|
|
34
43
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* Individual child processes may be scheduled concurrently on a multi-core CPU
|
|
46
|
+
* and separate processes will never share a port when connected to a service.
|
|
47
|
+
*
|
|
48
|
+
* @param numProcesses - The number of child processes to fork.
|
|
49
|
+
* @param cleanUpAccumulator - An array to accumulate cleanup functions for
|
|
50
|
+
* each child process. This is build per instance to accommodate any errors
|
|
51
|
+
* that might occur before completing all forking.
|
|
52
|
+
*
|
|
53
|
+
* @returns A promise that resolves with an object containing the child
|
|
54
|
+
* processes and a promise that rejects on any child process errors.
|
|
38
55
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
async function forkChildProcesses(
|
|
57
|
+
numProcesses: number,
|
|
58
|
+
cleanUpAccumulator: (() => void)[],
|
|
59
|
+
): Promise<{
|
|
60
|
+
children: ChildProcess[];
|
|
61
|
+
/**
|
|
62
|
+
* Will never resolve successfully, it is only used to reject on child process error.
|
|
63
|
+
*/
|
|
64
|
+
childErrorPromise: Promise<void>;
|
|
65
|
+
}> {
|
|
66
|
+
const children: ChildProcess[] = [];
|
|
67
|
+
const childReadyPromises: Promise<void>[] = [];
|
|
68
|
+
// Collect all child process error promises into this array
|
|
69
|
+
const childErrorPromises: Promise<void>[] = [];
|
|
70
|
+
// Fork child processes
|
|
71
|
+
for (let i = 0; i < numProcesses; i++) {
|
|
72
|
+
const child = fork("./lib/test/multiprocess/childClient.js", [
|
|
73
|
+
`child${i}` /* identifier passed to child process */,
|
|
74
|
+
]);
|
|
75
|
+
// Register a cleanup function to kill the child process
|
|
76
|
+
cleanUpAccumulator.push(() => {
|
|
77
|
+
child.kill();
|
|
78
|
+
child.removeAllListeners();
|
|
79
|
+
});
|
|
80
|
+
const readyPromise = new Promise<void>((resolve, reject) => {
|
|
81
|
+
child.once("message", (msg: MessageFromChild) => {
|
|
82
|
+
if (msg.event === "ack") {
|
|
83
|
+
resolve();
|
|
84
|
+
} else {
|
|
85
|
+
reject(
|
|
86
|
+
new Error(`Unexpected (non-"ack") message from child${i}: ${JSON.stringify(msg)}`),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
childReadyPromises.push(readyPromise);
|
|
92
|
+
const errorPromise = new Promise<void>((_, reject) => {
|
|
93
|
+
child.on("error", (error) => {
|
|
94
|
+
reject(new Error(`Child${i} process errored: ${error.message}`));
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
childErrorPromises.push(errorPromise);
|
|
48
98
|
|
|
49
|
-
|
|
99
|
+
child.send({ command: "ping" });
|
|
100
|
+
|
|
101
|
+
children.push(child);
|
|
102
|
+
}
|
|
103
|
+
// This race will be used to reject any of the following tests on any child process errors
|
|
104
|
+
const childErrorPromise = Promise.race(childErrorPromises);
|
|
105
|
+
|
|
106
|
+
// All children are always expected to connect successfully and acknowledge the ping.
|
|
107
|
+
await Promise.race([Promise.all(childReadyPromises), childErrorPromise]);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
children,
|
|
111
|
+
childErrorPromise,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
50
114
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
115
|
+
function composeConnectMessage(id: string | number): ConnectCommand {
|
|
116
|
+
return {
|
|
117
|
+
command: "connect",
|
|
118
|
+
user: {
|
|
119
|
+
id: `test-user-id-${id}`,
|
|
120
|
+
name: `test-user-name-${id}`,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function connectChildProcesses(
|
|
126
|
+
childProcesses: ChildProcess[],
|
|
127
|
+
readyTimeoutMs: number,
|
|
128
|
+
): Promise<{
|
|
129
|
+
containerCreatorAttendeeId: AttendeeId;
|
|
130
|
+
attendeeIdPromises: Promise<AttendeeId>[];
|
|
131
|
+
}> {
|
|
132
|
+
if (childProcesses.length === 0) {
|
|
133
|
+
throw new Error("No child processes provided for connection.");
|
|
134
|
+
}
|
|
135
|
+
const firstChild = childProcesses[0];
|
|
136
|
+
const containerReadyPromise = new Promise<{
|
|
137
|
+
containerCreatorAttendeeId: AttendeeId;
|
|
138
|
+
containerId: string;
|
|
139
|
+
}>((resolve, reject) => {
|
|
140
|
+
firstChild.once("message", (msg: MessageFromChild) => {
|
|
141
|
+
if (msg.event === "connected" && msg.containerId) {
|
|
142
|
+
resolve({
|
|
143
|
+
containerCreatorAttendeeId: msg.attendeeId,
|
|
144
|
+
containerId: msg.containerId,
|
|
145
|
+
});
|
|
78
146
|
} else {
|
|
79
|
-
|
|
80
|
-
message.containerId = await containerIdPromise;
|
|
147
|
+
reject(new Error(`Non-connected message from child0: ${JSON.stringify(msg)}`));
|
|
81
148
|
}
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
{
|
|
152
|
+
firstChild.send(composeConnectMessage(0));
|
|
153
|
+
}
|
|
154
|
+
const { containerCreatorAttendeeId, containerId } = await timeoutAwait(
|
|
155
|
+
containerReadyPromise,
|
|
156
|
+
{
|
|
157
|
+
durationMs: readyTimeoutMs,
|
|
158
|
+
errorMsg: "did not receive 'connected' from child process",
|
|
159
|
+
},
|
|
160
|
+
);
|
|
82
161
|
|
|
83
|
-
|
|
162
|
+
const attendeeIdPromises: Promise<AttendeeId>[] = [];
|
|
163
|
+
for (const [index, child] of childProcesses.entries()) {
|
|
164
|
+
if (index === 0) {
|
|
165
|
+
// The first child process is the container creator, it has already sent the 'connected' message.
|
|
166
|
+
attendeeIdPromises.push(Promise.resolve(containerCreatorAttendeeId));
|
|
167
|
+
continue;
|
|
84
168
|
}
|
|
85
|
-
|
|
86
|
-
|
|
169
|
+
const message = composeConnectMessage(index);
|
|
170
|
+
|
|
171
|
+
// For subsequent children, send containerId but do not wait for a response.
|
|
172
|
+
message.containerId = containerId;
|
|
87
173
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const errorPromise = new Promise<void>((_, reject) => {
|
|
97
|
-
child.on("error", (error) => {
|
|
98
|
-
reject(new Error(`Child${i} process errored: ${error.message}`));
|
|
174
|
+
attendeeIdPromises.push(
|
|
175
|
+
new Promise<AttendeeId>((resolve, reject) => {
|
|
176
|
+
child.once("message", (msg: MessageFromChild) => {
|
|
177
|
+
if (msg.event === "connected") {
|
|
178
|
+
resolve(msg.attendeeId);
|
|
179
|
+
} else if (msg.event === "error") {
|
|
180
|
+
reject(new Error(`Child process error: ${msg.error}`));
|
|
181
|
+
}
|
|
99
182
|
});
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
child.send(message);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (containerCreatorAttendeeId === undefined) {
|
|
190
|
+
throw new Error("No container creator session ID received from child processes.");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { containerCreatorAttendeeId, attendeeIdPromises };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Connects the child processes and waits for the specified number of attendees to connect.
|
|
198
|
+
* @remarks
|
|
199
|
+
* This function can be used directly as a test. Comments in the functionality describe the
|
|
200
|
+
* breakdown of test blocks.
|
|
201
|
+
*
|
|
202
|
+
* @param children - Array of child processes to connect.
|
|
203
|
+
* @param attendeeCountRequired - The number of attendees that must connect.
|
|
204
|
+
* @param childConnectTimeoutMs - Timeout duration for child process connections.
|
|
205
|
+
* @param attendeesJoinedTimeoutMs - Timeout duration for required attendees to join.
|
|
206
|
+
* @param earlyExitPromise - Promise that resolves/rejects when the test should early exit.
|
|
207
|
+
*/
|
|
208
|
+
async function connectAndWaitForAttendees(
|
|
209
|
+
children: ChildProcess[],
|
|
210
|
+
attendeeCountRequired: number,
|
|
211
|
+
childConnectTimeoutMs: number,
|
|
212
|
+
attendeesJoinedTimeoutMs: number,
|
|
213
|
+
earlyExitPromise: Promise<void> = Promise.resolve(),
|
|
214
|
+
): Promise<{ containerCreatorAttendeeId: AttendeeId }> {
|
|
215
|
+
// Setup
|
|
216
|
+
const attendeeConnectedPromise = new Promise<void>((resolve) => {
|
|
217
|
+
let attendeesJoinedEvents = 0;
|
|
218
|
+
children[0].on("message", (msg: MessageFromChild) => {
|
|
219
|
+
if (msg.event === "attendeeConnected") {
|
|
220
|
+
attendeesJoinedEvents++;
|
|
221
|
+
if (attendeesJoinedEvents >= attendeeCountRequired) {
|
|
222
|
+
resolve();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
108
226
|
});
|
|
109
227
|
|
|
110
|
-
//
|
|
228
|
+
// Act - connect all child processes
|
|
229
|
+
const connectResult = await connectChildProcesses(children, childConnectTimeoutMs);
|
|
230
|
+
|
|
231
|
+
Promise.all(connectResult.attendeeIdPromises)
|
|
232
|
+
.then(() => console.log("All attendees connected."))
|
|
233
|
+
.catch((error) => {
|
|
234
|
+
console.error("Error connecting children:", error);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Verify - wait for all 'attendeeConnected' events
|
|
238
|
+
await timeoutAwait(Promise.race([attendeeConnectedPromise, earlyExitPromise]), {
|
|
239
|
+
durationMs: attendeesJoinedTimeoutMs,
|
|
240
|
+
errorMsg: "did not receive all 'attendeeConnected' events",
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return connectResult;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* This particular test suite tests the following E2E functionality for Presence:
|
|
248
|
+
* - Announce 'attendeeConnected' when remote client joins session.
|
|
249
|
+
* - Announce 'attendeeDisconnected' when remote client disconnects.
|
|
250
|
+
*/
|
|
251
|
+
describe(`Presence with AzureClient`, () => {
|
|
252
|
+
const afterCleanUp: (() => void)[] = [];
|
|
253
|
+
|
|
254
|
+
// After each test, call any cleanup functions that were registered (kill each child process)
|
|
111
255
|
afterEach(async () => {
|
|
112
|
-
for (const child of children) {
|
|
113
|
-
child.kill();
|
|
114
|
-
}
|
|
115
|
-
children = [];
|
|
116
256
|
for (const cleanUp of afterCleanUp) {
|
|
117
257
|
cleanUp();
|
|
118
258
|
}
|
|
119
259
|
afterCleanUp.length = 0;
|
|
120
260
|
});
|
|
121
261
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
children[0].on("message", (msg: MessageFromChild) => {
|
|
128
|
-
if (msg.event === "attendeeConnected") {
|
|
129
|
-
attendeesJoinedEvents++;
|
|
130
|
-
if (attendeesJoinedEvents === numClients - 1) {
|
|
131
|
-
resolve();
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
durationMs,
|
|
138
|
-
errorMsg: "did not receive all 'attendeeConnected' events",
|
|
139
|
-
},
|
|
140
|
-
);
|
|
262
|
+
// Note that on slower systems 50+ clients may take too long to join.
|
|
263
|
+
const numClientsForAttendeeTests = [5, 20, 50, 100];
|
|
264
|
+
// TODO: AB#45620: "Presence: perf: update Join pattern for scale" may help, then remove .slice.
|
|
265
|
+
for (const numClients of numClientsForAttendeeTests.slice(0, 2)) {
|
|
266
|
+
assert(numClients > 1, "Must have at least two clients");
|
|
141
267
|
|
|
142
|
-
//
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.map(async (child, index) =>
|
|
152
|
-
timeoutPromise(
|
|
153
|
-
(resolve) => {
|
|
154
|
-
child.on("message", (msg: MessageFromChild) => {
|
|
155
|
-
if (
|
|
156
|
-
msg.event === "attendeeDisconnected" &&
|
|
157
|
-
msg.attendeeId === creatorSessionId
|
|
158
|
-
) {
|
|
159
|
-
resolve();
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
durationMs,
|
|
165
|
-
errorMsg: `Attendee[${index}] Disconnected Timeout`,
|
|
166
|
-
},
|
|
167
|
-
),
|
|
268
|
+
// Timeout duration used when waiting for response messages from child processes.
|
|
269
|
+
const childConnectTimeoutMs = 1000 * numClients;
|
|
270
|
+
const allConnectedTimeoutMs = 2000;
|
|
271
|
+
|
|
272
|
+
it(`announces 'attendeeConnected' when remote client joins session [${numClients} clients]`, async () => {
|
|
273
|
+
// Setup
|
|
274
|
+
const { children, childErrorPromise } = await forkChildProcesses(
|
|
275
|
+
numClients,
|
|
276
|
+
afterCleanUp,
|
|
168
277
|
);
|
|
169
278
|
|
|
170
|
-
|
|
171
|
-
|
|
279
|
+
// Further Setup with Act and Verify
|
|
280
|
+
await connectAndWaitForAttendees(
|
|
281
|
+
children,
|
|
282
|
+
numClients - 1,
|
|
283
|
+
childConnectTimeoutMs,
|
|
284
|
+
allConnectedTimeoutMs,
|
|
285
|
+
childErrorPromise,
|
|
286
|
+
);
|
|
287
|
+
});
|
|
172
288
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
289
|
+
it(`announces 'attendeeDisconnected' when remote client disconnects [${numClients} clients]`, async () => {
|
|
290
|
+
// Setup
|
|
291
|
+
const { children, childErrorPromise } = await forkChildProcesses(
|
|
292
|
+
numClients,
|
|
293
|
+
afterCleanUp,
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const connectResult = await connectAndWaitForAttendees(
|
|
297
|
+
children,
|
|
298
|
+
numClients - 1,
|
|
299
|
+
childConnectTimeoutMs,
|
|
300
|
+
allConnectedTimeoutMs,
|
|
301
|
+
childErrorPromise,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const childDisconnectTimeoutMs = 10_000;
|
|
305
|
+
|
|
306
|
+
const waitForDisconnected = children.map(async (child, index) =>
|
|
307
|
+
index === 0
|
|
308
|
+
? Promise.resolve()
|
|
309
|
+
: timeoutPromise(
|
|
310
|
+
(resolve) => {
|
|
311
|
+
child.on("message", (msg: MessageFromChild) => {
|
|
312
|
+
if (
|
|
313
|
+
msg.event === "attendeeDisconnected" &&
|
|
314
|
+
msg.attendeeId === connectResult.containerCreatorAttendeeId
|
|
315
|
+
) {
|
|
316
|
+
console.log(`Child[${index}] saw creator disconnect`);
|
|
317
|
+
resolve();
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
durationMs: childDisconnectTimeoutMs,
|
|
323
|
+
errorMsg: `Attendee[${index}] Disconnected Timeout`,
|
|
324
|
+
},
|
|
325
|
+
),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// Act - disconnect first child process
|
|
329
|
+
children[0].send({ command: "disconnectSelf" });
|
|
330
|
+
|
|
331
|
+
// Verify - wait for all 'attendeeDisconnected' events
|
|
332
|
+
await Promise.race([Promise.all(waitForDisconnected), childErrorPromise]);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
176
335
|
});
|